DE- AI acceleration
- Industries
- Finance
Nearshore software development for finance—secure, scalable, and compliant solutions for banking, payments, and APIs.
- Retail
Retail software development services—e-commerce, POS, logistics, and AI-driven personalization from nearshore engineering teams.
- Manufacturing
Nearshore manufacturing software development—ERP systems, IoT platforms, and automation tools to optimize industrial operations.
- Finance
- What we do
- Services
- Software modernization services
- Cloud solutions
- AI – Artificial intelligence
- Idea validation & Product development services
- Digital solutions
- Integrations for digital ecosystems
- A11y – Accessibility
- QA – Test development
- Technologies
- Front-end
- Back-end
- DevOps & CI/CD
- Cloud
- Mobile
- Collaboration models
- Collaboration models
Explore collaboration models customized to your specific needs: Complete nearshoring teams, Local heroes from partners with the nearshoring team, or Mixed tech teams with partners.
- Way of work
Through close collaboration with your business, we create customized solutions aligned with your specific requirements, resulting in sustainable outcomes.
- Collaboration models
- Services
- About Us
- Who we are
We are a full-service nearshoring provider for digital software products, uniquely positioned as a high-quality partner with native-speaking local experts, perfectly aligned with your business needs.
- Meet our team
ProductDock’s experienced team proficient in modern technologies and tools, boasts 15 years of successful projects, collaborating with prominent companies.
- Why nearshoring
Elevate your business efficiently with our premium full-service software development services that blend nearshore and local expertise to support you throughout your digital product journey.
- Who we are
- Our work
- Career
- Life at ProductDock
We’re all about fostering teamwork, creativity, and empowerment within our team of over 120 incredibly talented experts in modern technologies.
- Open positions
Do you enjoy working on exciting projects and feel rewarded when those efforts are successful? If so, we’d like you to join our team.
- Hiring guide
How we choose our crew members? We think of you as a member of our crew. We are happy to share our process with you!
- Rookie boot camp internship
Start your IT journey with Rookie boot camp, our paid internship program where students and graduates build skills, gain confidence, and get real-world experience.
- Life at ProductDock
- Newsroom
- News
Stay engaged with our most recent updates and releases, ensuring you are always up-to-date with the latest developments in the dynamic world of ProductDock.
- Events
Expand your expertise through networking with like-minded individuals and engaging in knowledge-sharing sessions at our upcoming events.
- News
- Blog
- Get in touch
22. Oct 2026 •7 minutes read
Can AI tell you whether a frozen project is actually safe to turn back on?
Bruno Raljić
Unit Lead & Integrations Service Lead at ProductDock
This is the third in a series about reviving a project that had sat untouched for years. The first post covered understanding the codebase, the second covered what project knowledge survived once the person who built it moved on; this one asks whether it’s actually safe to turn back on. Heads up: that answer lives in technical configuration and dependency detail, so this post runs more technical than the first two; we’ll do our best to keep it readable. Follow the series for the rest of the story.
Every team eventually inherits one of these: a system that works, that nobody has any specific complaint about, and that everybody still hesitates to expose to real traffic. Ask why, directly, and the honest answer is usually a shrug: it’s old, nobody’s checked it properly in years, better safe than sorry. That’s not a technical judgment. It’s a feeling dressed up as caution.
For this project, that feeling had a name and a specific shape once we actually went looking for it. Nobody had to decide anything about deploying it blindly; they just had to be handed a list of risks instead of a feeling. A feeling about risk and an actual list are not the same thing, and only one of them can move the needle.
If there’s a system in your own stack that everyone quietly avoids without anyone being able to say exactly why, this is that same hesitation, made concrete.

What we did
With the codebase already mapped from the first two sessions, we turned to a narrower question: not what does this do, but what would actually go wrong if it went live tomorrow. Same discipline as Session 1, static analysis first, no tools, no code touched:
- Full dependency audit, 18 backend dependencies and 12 frontend, each checked against its actual current release and its actual CVE history, not just how old it looked
SecurityConfig.javaand every properties file related to web security, read end to end, not just grepped for keywords- Every finding sorted into a prioritized upgrade list instead of one flat pile
- An updated, blunt verdict: Would we deploy this today?
The dependency side alone is split into three tiers: five things to fix before any public deployment; five more required before the framework upgrade this project needs anyway; and six real but not urgent. That sorting is the actual point. A client doesn’t need a hundred-line dependency report; they need to know which five things matter this week and which eleven are fine to schedule for later. All of it, reading both files end to end and sorting the result, took about half an hour.
The verdict itself didn’t move: still not safe to deploy. Post one already listed eight concrete reasons not to deploy this. This session didn’t shorten that list; it grew it to eleven, and sharpened one of the original eight into something worth its own telling.
One technical moment
Buried in SecurityConfig.java is a single security rule that, on its own, explains most of the rest of this post: the .permitAll() block lists /** right alongside the actual static-asset patterns it’s meant to whitelist, *.png, *.css, *.js. Spring Security evaluates its rules in the order they’re written; the first match wins. /** matches every URL there is, full stop, which means the very next line, .anyRequest().authenticated(), never runs. It isn’t buggy code. It’s dead code that still looks, at a glance, like a working lock on the door. In plain terms, a rule meant to leave a few windows open for public files ended up propping every door in the building wide open instead.
The likely story is mundane: someone wanted to whitelist a handful of static files, reached for a wildcard to catch anything they’d missed, and the wildcard ate the entire application instead. Method-level security is enabled in the framework config, with three separate modes, and used in exactly one place in the whole codebase: a single read-only “who am I” endpoint. Everything else, including three admin endpoints that wipe the database, delete every user, or re-hash every password, sits behind nothing but a plain, unauthenticated URL. Some of that was a conscious shortcut on an internal project that was never meant to carry real traffic, not pure oversight, but it’s still a real concern the moment going live is actually on the table.
In a follow-up pass, once the app was actually running and had a real test suite behind it, we came back to this with exact numbers instead of a description. Not every one of the app’s 26 HTTP endpoints across 9 controllers needs a login wall; plenty are meant to be public: standings, team pages, the login screen itself. When narrowed down to endpoints that are truly transactional, expose user data, or trigger admin actions, seven endpoints qualify. Grepped for the four annotations Spring actually uses to enforce access control at the method level, not one of the seven carries any. The one endpoint in the whole codebase that does is a read-only “who am I” check, confirmed to actually run at runtime, and it isn’t one of the seven; it’s the one place the team did think to lock something down. The seven include the three admin actions above, plus a target this pass hadn’t flagged before: an unauthenticated endpoint that writes real tickets and moves a user’s balance straight from request data. The URL-level rule that was supposed to catch all of this anyway is the same dead .anyRequest().authenticated() line from above.
One of those three admin endpoints deserved a sharper description than “needs a guard.” /auth/hash-pws BCrypt-encodes whatever is currently in each user’s password field and saves it, unconditionally. By the time anyone could actually call it, passwords are already BCrypt hashes, there’s no plaintext left to correctly re-hash. One unauthenticated GET request, the kind that fires from a plain image tag with no confirmation and no CSRF token required, permanently locks every account in the database at once. “Re-hashing” undersold it. To be clear, this isn’t the kind of thing we’d sign off on for production work, but on an internal side project moving fast toward an MVP, leaving a one-off admin utility like this unauthenticated was an understandable shortcut at the time, not negligence. It’s a different story the moment the app is actually meant to carry real users.
Where AI helped
Where it helped most was speed and recall on a close static read, spotting the kind of subtle ordering bug and obscure CVE history a scanner wouldn’t have flagged, without needing one. Where it needed a harder push was judgment: telling a dependency that’s silently pulled in from one that’s declared, and code that looks dangerous from code that actually is, both took a deliberate second check rather than a first-glance verdict.
The audit itself, both passes, came entirely from reading two files closely, no scanner run, no CVE database looked up for most of it. Log4j 1.x’s own CVE history, separate and distinct from the unrelated Log4Shell vulnerability three years its junior, came back correctly recalled, not looked up. The /** ordering bug came from reading SecurityConfig.java top to bottom the way a person would, not from a tool built to find exactly that pattern.
What a static read genuinely can’t do: see a dependency the project doesn’t declare directly but pulls in anyway, or confirm what’s actually reachable without the app running. At this point in the project, nothing had been built or run yet, so that limit was real, not a shortcut. In the follow-up pass, once the app was actually running and a real dependency tree could finally be pulled, one of those invisible dependencies turned out to matter: snakeyaml 1.30, three layers deep, carrying a CVE with a severity score of 9.8, arbitrary code execution through its YAML constructor. Nothing in pom.xml said snakeyaml was even in the build. The declared list was accurate. It also wasn’t the whole picture.
A second finding required the same discipline, pointed the other way: not assuming something is dangerous just because it looks dangerous. A cookie utility deserializes a client-supplied cookie using Java’s native deserialization, straight off an unauthenticated URL, the textbook shape of a remote-code-execution bug. Rather than reporting that shape as a finding on its own, the actual runtime dependency list was checked for the handful of libraries that the exploit chain needs to work. None of them was present. A real anti-pattern, a genuine landmine for whatever gets added to this project next, but not, right now, an exploitable one. Reporting a scary shape as if it were a confirmed exploit would have been the easier, lazier finding.
A live-looking credential elsewhere in the codebase demonstrated the opposite failure mode: a database connection string, complete with username and password, survived two entirely separate secrets sweeps, run at different points in this review, because it was sitting commented out inside an XML file everyone had already filed away as dead and safe to ignore. Neither pass was actually reading inside comments in a file nobody expected to matter. Orphaned and inert aren’t the same thing as “safe to stop reading.”
Why this matters if you’re hiring a nearshoring team
“The app is old and might be risky” isn’t something a team can act on, and it isn’t something a client can budget against either. “Seven endpoints are transactional, expose user data, or run admin actions, none of them check who’s asking, and here’s exactly which ones and what each does” is a different kind of statement entirely. It isn’t just faster, it’s actionable, and it doesn’t ask anyone to trust a feeling about how careful the audit was.
That precision is the actual product, not the speed at which it arrived. Anyone with an AI subscription can generate a quick opinion on a codebase today, which stopped being a differentiator the moment it became available to everyone. What doesn’t come free is knowing which five dependency findings out of sixteen need fixing before anything else touches this app, which eleven of those sixteen are fine to schedule for later, and which one deserves a sharper sentence than “needs a guard” because a single request can permanently lock every account in the database. That’s judgment applied at the new speed, not the speed by itself.
This also changes which clients are worth taking on in the first place. A frozen codebase nobody has opened in years used to be a hard sell, too much unknown risk for too little certainty about what’s underneath. A session that turns that unknown into a scored, prioritized list changes the shape of that conversation. The project doesn’t get any younger. It gets a specific, defensible answer to “is it safe,” and that answer is worth more than the guess it replaced.
What’s next
A prioritized list of what’s wrong doesn’t give you anything to prove a fix didn’t break something else. The next post covers building that from zero: a real test suite on a codebase that started with effectively no coverage at all, and what it actually took to get there.
Stay tuned for the rest of the series, next up: what it takes to make a codebase safe to change, not just safe to read.
Tags:Skip tags
Bruno Raljić
Unit Lead & Integrations Service Lead at ProductDockBruno has been in software since 2011, starting as a developer and growing into a role that sits between people, clients, and delivery. At ProductDock, he builds and leads teams, works directly with clients, and focuses on the human side of software projects. He believes that in the age of AI, the people on a project matter more than ever, not less.