Common Login Portal
Angular 18 to 21: 48 vulnerabilities down to 2
Ashish Bhagat · 13 Aug 2026
Before:
npm audit
48 vulnerabilities (28 high)
After upgrading CLP's frontend from Angular 18 to 21:
npm audit
2 vulnerabilities (moderate)
CLP's frontend — around 152 TypeScript and HTML files, roughly 15,300 lines, 62 Angular components — went through this upgrade mid-project, not as a rewrite, not as a nice-to-have cleanup. It came out of an internal VAPT cycle that also covered CSRF protection, encryption at rest, rate limiting, and header hardening across both repos. An npm audit backlog that size doesn't pass a real security review quietly, and it isn't the kind of thing you get to explain away in front of an auditor with "most of those are dev dependencies."
Four majors, not one jump
It's worth naming the actual trade-off this upgrade represents, because "just upgrade" undersells it. A four-major-version jump on the framework your entire frontend is built on is real, non-trivial risk on a project with a fixed scope and no dedicated QA team beyond whatever coverage exists — every migration is a chance to introduce a regression in a codebase that also has to keep working for a payment flow, a multi-step registration wizard, and an SSO handoff, none of which get to break while the framework underneath them changes out from under them.
Angular 18 to 21 isn't a single version bump — it's four major releases apart, and Angular's own upgrade tooling isn't designed to skip between them. ng update runs each major version's migration schematics against your code, and those schematics are written and tested against the assumption that you're one major version away from your target, not several. Getting from 18 to 21 in practice means running ng update @angular/core@19 @angular/cli@19, letting that land and fixing whatever it flags, then repeating for 20, then 21 — four smaller, well-defined migrations chained together, rather than one attempt to leap the whole distance at once. Skipping majors isn't a shortcut Angular's tooling actually supports; it's a way to end up with a codebase in an unsupported in-between state.
Each of those four migrations also carries its own set of framework-level changes to absorb, not just dependency bumps — standalone components becoming the default project shape, the newer built-in control-flow syntax replacing structural directives, signals maturing as the preferred way to model reactive state. None of that is optional scope you can skip by only caring about the security angle; the schematics that update your dependencies are the same schematics that touch your templates and component decorators, and skipping a step to save time just means finding out what broke during the next one instead.
The practical discipline that made this survivable was treating each of the four steps as its own complete migration: run the schematics, fix whatever they flag or whatever the build breaks on, run the full QA suite against that intermediate version, and only then move to the next major. Jumping straight from 18 to 21's dependency versions in package.json without walking through 19 and 20's migrations first tends to leave a project in a state Angular's own tooling never actually tested for — the schematics assume they're running against the previous major's conventions, not whatever's two or three versions behind.
Where the vulnerabilities actually were
None of that makes the count meaningless, and it's worth resisting the urge to wave 48 away as "just dev dependencies" without checking. Some findings genuinely do sit in code that ships to the browser — dependencies bundled into the actual application rather than only used to build it — and those deserve to be triaged individually rather than assumed away along with the build-tooling noise.
A number like 48 vulnerabilities in a frontend project's npm audit output is almost never 48 distinct holes in code the application ships to a browser. The large majority of findings in an Angular project this size typically trace back to the build toolchain's own transitive dependencies — the CLI, its bundler, its loaders — rather than anything in the application's runtime bundle. That doesn't make them irrelevant; a vulnerable dev-tooling dependency is still a supply-chain risk worth closing, and it's exactly the kind of finding a VAPT report will list regardless of whether it's reachable from a browser. But it does explain why a major-version bump of the framework and its CLI can collapse a large chunk of the count in one move: pulling current major versions of Angular and its build tooling pulls current major versions of everything underneath them too, resolving whole clusters of transitive findings at once instead of patching them individually.
Two moderate, and stopping there
Two moderate findings remained after the full 18-to-21 migration. Zero wasn't the bar — a defensible bar for a mid-project security pass is closing everything that represents real exposure and having a specific, stated reason for anything left open, not chasing an empty npm audit output as an end in itself while the rest of the project waits. The honest version of this exercise isn't "we got to zero," it's "we went from a backlog that would fail a review to a small, named residual that a reviewer can look at directly and judge for themselves" — which is a very different, and more credible, thing to hand a pentester than a clean scan you can't fully account for.
Why this wasn't optional
A government-facing identity platform doesn't get to treat a stale npm audit output as background noise the way a lower-stakes internal tool might. This upgrade came out of the same internal VAPT cycle that covered CSRF protection, encryption at rest, rate limiting, and header hardening — the kind of review where a reviewer runs npm audit as a matter of course and expects either a clean result or a specific, defensible explanation for anything that isn't. "We're aware, and here's why the remaining findings don't matter" is a fine answer to give a reviewer. "We haven't looked" is not, and a 48-vulnerability backlog with 28 high-severity findings reads exactly like the second answer whether or not it's true.
What I'd do differently
This landed as one mid-project batch upgrade, four majors deep, because dependency versions had been left to drift while feature work took priority — a completely normal thing to happen on a solo build with a fixed scope and a deadline, and also exactly the situation that turns a routine upgrade into a four-stage migration instead of a series of small ones. If I were running this again, I'd treat Angular major-version bumps as part of the regular delivery cadence — landing each one within a release or two of it shipping — so a security pass never has to absorb four versions of drift in a single sitting.
More from this series:
Fixing IDOR properly · Mock and real adapters behind every port