Safeguard
Vulnerability Analysis•

Angular SSR CVE-2026-111752 and CVE-2026-111754: router DoS, open redirect

Two Angular SSR bugs: one URL can crash the SSR worker, another makes an open redirect. Fixed in 20.3.33, 21.2.25 and 22.2.1; Angular 19 gets no fix.

Safeguard Security Team
43 min read

CVE-2026-111752 lets one crafted URL make @angular/router exhaust the Node.js heap during server-side rendering. CVE-2026-111754 turns @angular/platform-server into an open redirect. Both affect only apps that render on a server. Update to 20.3.33, 21.2.25 or 22.2.1. Angular 19 and older get no fix, so filter requests and plan the upgrade.

The Angular team shipped both fixes on 30 September 2026 and published its own advisories that evening. On 20 October GitHub reviewed them into the GitHub Advisory Database, which is the moment npm audit and Dependabot started flagging them across many projects. That two week gap is why many teams are only now seeing these IDs. As of 25 October, CVE.org still has no published records for CVE-2026-111752 or CVE-2026-111754, or for the related CVE-2026-111751 and CVE-2026-111753, so the GitHub advisories remain the records to cite.

This post covers what each bug does, who is affected, the fix commits on each line, the related SSR advisories, a lockfile checker, a request guard for teams that cannot upgrade today, and what Angular 19 users should do now.

The short answer

If your Angular app renders on a Node.js server, update @angular/router and @angular/platform-server to 20.3.33, 21.2.25 or 22.2.1 or later. CVE-2026-111752 (CVSS 4.0 score 8.2, high) lets an unauthenticated request with crafted auxiliary outlet groups exhaust the SSR worker's heap. CVE-2026-111754 (5.1, moderate) lets a path with a dot segment and extra slashes produce a 302 redirect to another domain. Browser-only apps and build-time prerendering are not affected. Angular 19 and older have no fixed version. Until you can upgrade, reject outlet syntax and dot-segment tricks in front of SSR, as the advisories recommend.

The current patch releases are newer still. On 25 October the npm latest tag for @angular/router was 22.2.3, the v21-lts tag 21.2.25 and the v20-lts tag 20.3.34. Going to those versions covers both CVEs in this post, plus CVE-2026-111751 on the router and, for Angular 20, CVE-2026-111753 in @angular/common.

What happened

The two bugs were fixed in the same 24 hours, released together, and published as separate advisories. The timeline below is built from GitHub's issue, pull request and commit records, npm's publish times and the advisory metadata. All times are UTC.

Date and time (UTC, 2026)EventSource
25 Sep, 05:58Issue angular/angular#70932 opened by SkyZeroZx: "auxiliary outlets can exhaust memory during SSR"GitHub issue #70932
28 Sep, 04:17Issue angular/angular#70997 opened by SkyZeroZx: "zero-segment group replay can create duplicate outlet siblings and exhaust SSR workers"GitHub issue #70997
29 Sep, 19:05Router fix commit authored by Andrew Scott: "require outlets to match a route before processing child segments"Commit b6bf650
29 Sep, 19:26Pull request #71055 opened by atscottGitHub PR #71055
30 Sep, 04:50Issue angular/angular#71076 opened by SkyZeroZx: "SSR pathname normalization can turn a same-origin navigation into an open redirect"GitHub issue #71076
30 Sep, 08:35Platform-server fix commit authored by Alan Agius: "reject protocol-relative paths in resolveUrl"; PR #71077 opened within the minute, backport PRs #71078 (08:39) and #71079 (08:42)Commit d7d81e1, PRs #71077 to #71079
30 Sep, 14:19 to 14:21Platform-server fix merged on main, 20.3.x and 21.2.xGitHub PRs #71077 to #71079
30 Sep, 14:39PR #71055 merged; router fix commits land on each branch between 14:39 and 14:42GitHub PR #71055, commits
30 Sep, 17:56 to 18:37@angular/router 20.3.33 (17:56), 21.2.25 (18:22) and 22.2.1 (18:37) published to npmnpm registry
30 Sep, 19:29Angular publishes GHSA-w739-gvwx-grc3 (CVE-2026-111754) in its repositoryAngular security advisories
30 Sep, 19:32Angular publishes GHSA-62vg-58rm-qff7 (CVE-2026-111752)Angular security advisories
1 Oct, 05:33Angular publishes GHSA-57xq-rjx2-v5xh (CVE-2026-111751), a separate SSR denial of service in RouterLinkAngular security advisories
14 Oct, 23:20 to 23:4120.3.34 and 22.2.3 published; 20.3.34 includes the HTTP client XSRF fixnpm registry, Angular changelog
16 Oct, 07:43Angular publishes GHSA-jfgx-762w-99vr (CVE-2026-111753), XSRF token leakage in @angular/commonAngular security advisories
20 Oct, 14:35:49GitHub reviews GHSA-w739-gvwx-grc3 into the GitHub Advisory DatabaseGitHub Advisory Database
20 Oct, 14:36:39GitHub reviews GHSA-62vg-58rm-qff7GitHub Advisory Database
20 Oct, 17:52 and 20:37GitHub reviews CVE-2026-111751 and CVE-2026-111753GitHub Advisory Database
25 OctCVE.org's API returns no record for CVE-2026-111751, -111752, -111753 or -111754CVE Services API

Two details in that table matter for planning. The fixes have been on npm since 30 September, so any project that took a routine patch update in early October already has them. And the advisories reached the GitHub database only on 20 October, so automated tools that rely on it stayed quiet for almost three weeks.

Who is affected

Both CVEs need server-side rendering. If your Angular app only runs in the browser, neither applies. The router advisory makes the reason explicit: in a browser, the memory blowup happens in the tab of whoever typed the URL, which crosses no security boundary. The platform-server advisory notes that ServerPlatformLocation is only used on the server.

PackageAffected rangeFirst fixed versionNotes
@angular/router (CVE-2026-111752)22.0.0 up to but not including 22.2.122.2.1Active line
@angular/router (CVE-2026-111752)21.0.0 up to but not including 21.2.2521.2.25LTS until 2027-06
@angular/router (CVE-2026-111752)20.0.0 up to but not including 20.3.3320.3.33LTS until 2026-11-28
@angular/router (CVE-2026-111752)19.2.25 and earlierNoneEnd of support, "will not be patched"
@angular/platform-server (CVE-2026-111754)22.0.0 up to but not including 22.2.122.2.1Active line
@angular/platform-server (CVE-2026-111754)21.0.0 up to but not including 21.2.2521.2.25LTS
@angular/platform-server (CVE-2026-111754)20.0.0 up to but not including 20.3.3320.3.33LTS
@angular/platform-server (CVE-2026-111754)19.2.25 and earlierNoneEnd of support

Conditions for CVE-2026-111752

The router advisory lists four conditions, and an application is affected only when all of them hold:

  1. SSR is enabled and the app renders on demand on a Node.js (V8) server.
  2. At least one level of the route configuration has an empty-path route such as path: "", or a route assigned to a named outlet. Empty-path routes are common: layout shells, default child routes and redirect stubs all use them.
  3. Request lines reach Node.js without an edge proxy rejecting them first. The advisory notes that Node.js accepts request headers up to 16 KiB by default, and that the guard and resolver amplification needs only a short URL.
  4. The worker heap is small enough relative to the request size and concurrency. The advisory gives containers or serverless instances with 128 MiB to 1 GiB as examples.

The fourth condition decides how bad this is for you. A large heap needs more requests to fall over; a small serverless memory tier needs very few.

Conditions for CVE-2026-111754

The platform-server advisory lists three:

  1. SSR is enabled using @angular/platform-server and @angular/ssr.
  2. The router has parameter routes with wildcard or child segments, for example a /:tenant route that will happily match a single dot, or the app passes user-provided navigation targets such as a returnUrl query parameter to the router.
  3. Upstream proxies pass semicolons or dot segments through without stripping or normalizing them. The advisory names a web server, a CDN and a cloud load balancer as examples, which in practice means every hop in front of your SSR servers is worth checking.

Who is not affected

  • Browser-only single page applications, whatever their version.
  • Pages prerendered at build time. The router advisory says build-time prerendering does not process attacker-supplied request URLs.
  • Apps where every request passes through a proxy that already rejects parentheses in paths and normalizes dot segments. This is less common than people assume; check rather than guess.

How it works, in plain language

CVE-2026-111752: one URL, many route trees

Angular URLs can carry auxiliary outlet groups in parentheses. They let one URL describe a primary route and secondary routes rendered alongside it, such as a detail panel. The router parses those groups and matches each one against the route configuration.

According to the advisory, the router did not check whether an outlet group in the URL matched a route configured for that outlet name. An empty group, or a group naming an outlet the app never configured, was not rejected. Instead the router evaluated the route configuration again for each such group. Nest enough of them in one URL and the router builds duplicate route snapshot trees, runs canMatch guards and resolvers again and again, and copies the route parameters and query parameters into each snapshot.

The memory is transient. Once route recognition finishes it is released, and there is no lasting leak. The problem is that recognition may never finish: the advisory says a single crafted request, or a few concurrent ones, can push the Node.js heap past its limit and kill the SSR worker with a fatal out-of-memory error. If the matched routes have guards or resolvers that call your backend, those services take the repeated calls too, even from a short URL that would never exhaust memory.

CVE-2026-111754: a check on the wrong string

When Angular SSR renders a request, ServerPlatformLocation resolves the request URL with a function called resolveUrl (or parseUrl on the 20.x line). That function already refused protocol-relative URLs, the kind that start with two slashes and that browsers treat as "same scheme, different host".

The advisory explains that the refusal checked the raw string before WHATWG URL normalization. A path like /.//evil.test does not start with two slashes, so it passed. Normalization then removed the /. dot segment and left a pathname of //evil.test. When @angular/ssr noticed that the rendered URL and the router's final location disagreed, it issued an HTTP 302 with that pathname in the Location header. Browsers read Location: //evil.test as a jump to another site.

That is a classic open redirect. It does not leak data or run code, but it gives phishing links a believable start, your domain. The advisory adds that application checks of the form "does returnUrl start with //" accept these inputs as safe.

Technical details

All values in this section come from the GitHub advisories, Angular's repository advisories, the fix commits and the npm registry, as fetched on 25 October 2026.

Identifiers and scores

FieldCVE-2026-111752CVE-2026-111754
GHSAGHSA-62vg-58rm-qff7GHSA-w739-gvwx-grc3
Package@angular/router@angular/platform-server
Title (GitHub)Denial of Service via Unmatched Empty-Path Outlet Route Matching in SSROpen Redirect via Protocol-Relative URLs in SSR
GitHub severityHighModerate
CVSS 4.0 score8.25.1
CVSS 4.0 vectorCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N
CVSS 3.xNot providedNot provided
CWECWE-400 Uncontrolled Resource Consumption; CWE-405 Asymmetric Resource Consumption (Amplification)CWE-601 URL Redirection to Untrusted Site (Open Redirect)
Credits (GitHub)SkyZeroZx (remediation developer), atscott (remediation reviewer), alan-agius4 (other); no finder listedSkyZeroZx (finder), alan-agius4 (remediation developer), JeanMeche (remediation reviewer)
Repository advisory published2026-09-30 19:32:57 UTC2026-09-30 19:29:39 UTC
GitHub Advisory Database review2026-10-20 14:36:39 UTC2026-10-20 14:35:49 UTC
CVE.org recordNone on 25 OctNone on 25 Oct
NVDNo record, since there is no published CVE recordNo record
CISA KEVNot listedNot listed

Read the vectors rather than the labels. The router bug has availability impact only, high, with an attack requirement (AT:P) that reflects the route configuration and heap size preconditions. The redirect has no confidentiality or availability impact on your system, a low integrity and confidentiality impact on the subsequent system (the user's next destination), and needs user interaction, because someone has to click the link.

Fix commits by release line

LineRouter fix (CVE-2026-111752)Platform-server fix (CVE-2026-111754)Release
mainb6bf6504adab1a78cc62ad8402f6bed229e620bdd7d81e189d227108c3b9d39178dbb7389f6f970dnext releases
22.2.xe9bf52aa03a3b9b302c33ffbc967ef0e1de5fb79928dcfd2582152655b5559509d057bf6a5a1952122.2.1
21.2.xa83f8119acd466b524ae0113096ed188358d6d6c450af9c813c7660b9bd91e5635f705ee3ed7b32a21.2.25
20.3.xb0d2ba0f54e21cb57ea5a0ca0d8e6065f28e23a083944fc8c50e86be90e8ec4184cd15002b5af26220.3.33

The advisories list four router commits and four platform-server commits. Angular's changelog places them: the 22.2.1 entry names e9bf52aa03 and 928dcfd258, the 21.2.25 entry names a83f8119ac and 450af9c813, and the 20.3.33 entry names b0d2ba0f54 and 83944fc8c5. The other two are the original commits on main, from pull requests #71055 and #71077.

What the router fix changes

The router commit touches packages/router/src/recognize.ts and packages/router/src/utils/config_matching.ts, plus 160 lines of new tests in recognize.spec.ts. Its commit message describes four gaps that together let unconfigured zero-segment outlet groups through:

  1. processChildren passed every child outlet to processSegmentGroup, which sent any zero-segment group with children straight back to processChildren against the same parent configuration, without matching a route. The fix calls processSegment directly, so only the root segment group takes that shortcut.
  2. processSegmentAgainstRoute let named outlets pierce empty-path routes for other outlets even when the route had no children to pierce into, or the URL group had nothing to pierce with. The fix throws NoMatch in both cases.
  3. matchSegmentAgainstRoute could branch into child processing, or return a finished leaf match, before checking that the route's outlet was the one being matched. The fix checks the outlet first and requires a real descendant match when piercing an empty-path route for another outlet.
  4. noLeftoversInUrl checked only whether the segment group had a child for the current outlet. It now checks whether the group has any children at all, so an unmatched group with children for other outlets counts as leftover and fails.

The commit says it fixes both #70932 and #70997, so it closes two related fan-out paths. The 20.3.33 and 21.2.25 changelogs list two more router hardening commits in the same releases: "do not copy URL-sized objects into every route snapshot" and "reject duplicate outlets in production builds". The 22.2.1 changelog adds two more router fixes on the active line: "fall back when serializing protocol-relative URLs" and "limit absolute redirects from redirect functions". The duplicate outlet commit matters because the public issue pointed out that Angular's check for two siblings on the same outlet (error NG04006) only ran in development mode, so a production build accepted route trees a development build would have rejected.

What the platform-server fix changes

The platform-server commit changes packages/platform-server/src/url.ts and its spec. It adds a helper, isDisallowedProtocolRelative, that returns true when protocol-relative URLs are not allowed and the normalized URL.pathname starts with //. resolveUrl now calls it after WHATWG resolution, on both code paths, and throws the existing PROTOCOL_RELATIVE_URL_NOT_ALLOWED runtime error. The old raw-string check stays in place. In short, the check moved from the string the attacker controls to the string the browser will actually use.

Measurements published in the public issue

Issue #70997, which the router advisory links, includes measurements its author took on Angular 22.2.0 and Node.js 24.16.0 with outputMode: "server", against an app with a three-panel route configuration. We have not reproduced them, and they describe one configuration, but they show why the heap size condition matters:

Request path size128 MiB heap256 MiB heap512 MiB heap
7,669 bytes10 requests20 requestsover 40 requests
15,349 bytes6 requests10 requests20 requests

The issue reports that every failure was V8 hitting its own heap limit, and that a development build answered 404 and stayed healthy. That last point is the reason a local test with ng serve can make you think you are not affected when your production build is. In an ablation the author ran, enabling the duplicate outlet check in a production build dropped peak RSS from 296 MiB to 73 MiB for the same four concurrent requests at a 256 MiB heap.

Request shapes and log patterns

The advisories publish example request lines so defenders can build filters. They are not secret, and they are what you would search your logs for:

  • Router: request paths containing auxiliary outlet groups in parentheses, and in particular the empty-path outlet sequences the advisory names: :/(, (/ and ///(. Legitimate traffic to an app that never uses named outlets in its URLs should contain no parentheses in the path at all.
  • Platform-server: paths containing /.//, /..// or /.;/, and SSR responses with status 302 whose Location header starts with //.
  • Crash side: Node.js workers exiting with "JavaScript heap out of memory", or container restarts with out-of-memory reasons, that line up with requests carrying long parenthesized paths.

No indicators of compromise, attacker infrastructure or in-the-wild exploitation have been published for either CVE. Neither is on CISA's Known Exploited Vulnerabilities list.

Part of a run of SSR fixes

These two are not isolated. Between late August and mid October, Angular's two main repositories published a string of advisories against the server rendering stack. Updating to the latest patch on your major covers all of them.

CVEPackageIssueFixed in
CVE-2026-88056@angular/platform-serverSSRF and cross-origin credential disclosure via URL resolution discrepancy20.3.30, 21.2.22, 22.1.4
CVE-2026-88058, CVE-2026-88060@angular/platform-serverSSR cross-site scripting in fallback raw-content elements20.3.30, 21.2.22, 22.1.4
CVE-2026-104871@angular/ssrPath traversal to sibling directories in CommonEngine on Windows20.3.36, 21.2.23, 22.1.7
CVE-2026-101895@angular/platform-serverInfinite loop on a malformed DOCTYPE20.3.31, 21.2.23, 22.1.6
CVE-2026-101896@angular/routerDenial of service via numeric URL matrix parameters20.3.32, 21.2.24, 22.2.0
CVE-2026-111752@angular/routerOutlet route matching denial of service (this post)20.3.33, 21.2.25, 22.2.1
CVE-2026-111754@angular/platform-serverProtocol-relative open redirect (this post)20.3.33, 21.2.25, 22.2.1
CVE-2026-111751@angular/routerDenial of service via RouterLink query parameter retention; introduced in 21.2.021.2.25, 22.2.1
CVE-2026-111753@angular/commonXSRF token leakage, incomplete fix for CVE-2025-66035; browser-side, not SSR20.3.34, 21.0.3; 22.x not affected

Every advisory in this table lists 19.2.25 and earlier as affected with no patched version (19.2.27 and earlier for @angular/ssr), except CVE-2026-111751, which was introduced in 21.2.0 and never affected older lines. The router advisory for CVE-2026-111752 spells the policy out: "End of Support / LTS expired; will not be patched". CVE-2026-111753 is worth a separate note: its advisory says the 21.0.3 and later releases and all of 22.x had already replaced the vulnerable check, and only the 20.3.x line kept it until 20.3.34. If you are on Angular 20, 20.3.33 is not enough; 20.3.34 is the release that covers everything in this table.

The Angular 19 problem

Angular's release page lists v22 as active, v21 in long-term support until June 2027 and v20 in long-term support until 28 November 2026. It states plainly that versions 2 to 19 are no longer supported. The advisories follow that policy: no 19.x fix, and the last @angular/router 19 release on npm is still 19.2.25.

That leaves a lot of projects without a patch to install. npm's per-version download counts for the week before 24 October show 3,263,290 downloads of @angular/router, of which 25.9% were for versions 19 and older (6.8% for 19 alone). The picture for @angular/platform-server, the package that only SSR apps install, is similar: 25.0% of its 490,327 weekly downloads were 19 or older. Downloads include CI runs and mirrors, so this overstates live applications, but the share is too large to wave away.

Another 38% or so were on supported majors but below the fixed release, which a patch bump fixes. Roughly 36% were already fixed.

Angular 20 users have a date of their own. Its LTS ends on 28 November 2026, five weeks from the date of this post. Moving to 20.3.34 today is right, but it is the last stop on that line. Plan the move to 21 or 22 now, so that the next SSR advisory after 28 November does not find you where Angular 19 users are today.

How to check whether you are exposed

Step 1: does the app render on a server?

Look for the signs of SSR in the workspace, not just the package list. On the application builder, angular.json will have an ssr entry for the build target, and outputMode set to "server" for on-demand rendering. Older setups use @nguniversal/express-engine and a separate server build.

# Server rendering config in the workspace
grep -nE '"(outputMode|ssr|server|prerender)"' angular.json

# Server entry points
ls src/server.ts server.ts src/main.server.ts 2>/dev/null

# SSR packages installed
npm ls @angular/ssr @angular/platform-server @nguniversal/express-engine

If outputMode is "static" and every route is prerendered, the request-time bugs here do not apply to those pages. If any route uses RenderMode.Server in your server routes file, that route renders on demand and is in scope.

Step 2: which versions are installed?

npm ls shows the resolved versions, including duplicates pulled in by libraries:

npm ls @angular/router @angular/platform-server @angular/common --all
pnpm why @angular/router
yarn why @angular/router

For many repositories at once, the script below reads package-lock.json files directly. It covers the two CVEs in this post plus CVE-2026-111751 and CVE-2026-111753, finds nested copies, and tags which findings are SSR only. It uses the Python standard library only.

#!/usr/bin/env python3
"""Check npm lockfiles for the Angular SSR advisories reviewed by GitHub on 20 October 2026.

CVE-2026-111752  @angular/router           fixed 20.3.33, 21.2.25, 22.2.1; 19 and older: no fix
CVE-2026-111754  @angular/platform-server  fixed 20.3.33, 21.2.25, 22.2.1; 19 and older: no fix
CVE-2026-111751  @angular/router           affects 21.2.0+ and 22.x; fixed 21.2.25, 22.2.1
CVE-2026-111753  @angular/common           fixed 20.3.34 and 21.0.3; 22.x not affected; 19 and older: no fix
Reads package-lock.json (lockfile version 2 or 3), including nested copies.
Standard library only. Usage: python3 angular_ssr_check.py path/to/package-lock.json [...]
"""
import json
import sys

RULES = {
    "@angular/router": [
        ("CVE-2026-111752", {20: (20, 3, 33), 21: (21, 2, 25), 22: (22, 2, 1)}, None, True),
        ("CVE-2026-111751", {21: (21, 2, 25), 22: (22, 2, 1)}, (21, 2, 0), True),
    ],
    "@angular/platform-server": [
        ("CVE-2026-111754", {20: (20, 3, 33), 21: (21, 2, 25), 22: (22, 2, 1)}, None, True),
    ],
    "@angular/common": [
        ("CVE-2026-111753", {20: (20, 3, 34), 21: (21, 0, 3), 22: (22, 0, 0)}, None, False),
    ],
}
SSR_PACKAGES = ("@angular/ssr", "@nguniversal/express-engine")
SUPPORT = {20: "LTS ends 2026-11-28", 21: "LTS ends 2027-06", 22: "active, LTS ends 2028-06"}


def vtuple(v):
    core = v.split("-")[0].split("+")[0]
    return tuple(int(x) for x in core.split(".")[:3])


def findings(name, version):
    v = vtuple(version)
    major = v[0]
    out = []
    for cve, fixed, introduced, ssr_only in RULES[name]:
        tag = f"{cve} (SSR only)" if ssr_only else cve
        if introduced and v < introduced:
            continue
        if major not in fixed:
            if major <= 19 and not (introduced and introduced[0] > 19):
                out.append(f"{tag}: no fix for {major}.x")
            continue
        if v < fixed[major]:
            out.append(f"{tag}: update to {'.'.join(map(str, fixed[major]))}")
    return out


def check(lock_path):
    with open(lock_path, encoding="utf-8") as fh:
        pkgs = json.load(fh).get("packages", {})
    lines = []
    ssr = sorted({p.rsplit("node_modules/", 1)[1] for p in pkgs
                  if p.rsplit("node_modules/", 1)[-1] in SSR_PACKAGES})
    lines.append(f"  SSR packages: {', '.join(ssr) if ssr else 'none found (SSR only CVEs need a server build)'}")
    for path, meta in sorted(pkgs.items()):
        name = path.rsplit("node_modules/", 1)[-1]
        if name not in RULES or "version" not in meta:
            continue
        version = meta["version"]
        major = vtuple(version)[0]
        issues = findings(name, version)
        verdict = "; ".join(issues) if issues else "ok"
        support = SUPPORT.get(major, "unsupported major")
        where = "" if path == f"node_modules/{name}" else f"  [{path}]"
        lines.append(f"  {name:<25} {version:<8} {verdict}  ({support}){where}")
    return lines


if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit(__doc__)
    for p in sys.argv[1:]:
        print(f"== {p}")
        print("\n".join(check(p)))

We ran it against five made-up lockfiles: an SSR shop on 21.2.20, a portal on 20.3.33, a legacy SSR app on 19.2.25, a browser-only app on 22.1.0 and an SSR app on the current 22.2.3.

$ python3 angular_ssr_check.py shop/package-lock.json portal/package-lock.json legacy/package-lock.json spa/package-lock.json current/package-lock.json
== shop/package-lock.json
  SSR packages: @angular/ssr
  @angular/common           21.2.20  ok  (LTS ends 2027-06)
  @angular/platform-server  21.2.20  CVE-2026-111754 (SSR only): update to 21.2.25  (LTS ends 2027-06)
  @angular/router           21.2.20  CVE-2026-111752 (SSR only): update to 21.2.25; CVE-2026-111751 (SSR only): update to 21.2.25  (LTS ends 2027-06)
== portal/package-lock.json
  SSR packages: @angular/ssr
  @angular/common           20.3.33  CVE-2026-111753: update to 20.3.34  (LTS ends 2026-11-28)
  @angular/platform-server  20.3.33  ok  (LTS ends 2026-11-28)
  @angular/router           20.3.33  ok  (LTS ends 2026-11-28)
== legacy/package-lock.json
  SSR packages: @angular/ssr
  @angular/common           19.2.25  CVE-2026-111753: no fix for 19.x  (unsupported major)
  @angular/platform-server  19.2.25  CVE-2026-111754 (SSR only): no fix for 19.x  (unsupported major)
  @angular/router           19.2.25  CVE-2026-111752 (SSR only): no fix for 19.x  (unsupported major)
== spa/package-lock.json
  SSR packages: none found (SSR only CVEs need a server build)
  @angular/common           22.1.0   ok  (active, LTS ends 2028-06)
  @angular/router           22.1.0   CVE-2026-111752 (SSR only): update to 22.2.1; CVE-2026-111751 (SSR only): update to 22.2.1  (active, LTS ends 2028-06)
== current/package-lock.json
  SSR packages: @angular/ssr
  @angular/common           22.2.3   ok  (active, LTS ends 2028-06)
  @angular/platform-server  22.2.3   ok  (active, LTS ends 2028-06)
  @angular/router           22.2.3   ok  (active, LTS ends 2028-06)

In a monorepo where the SSR server is its own package, run it against the lockfile that the server build actually installs from.

Step 3: does the route configuration meet the router preconditions?

Search for empty-path routes and named outlets. Almost every real application has at least one empty-path route, so expect a hit and treat it as "condition met".

grep -rnE "path:\s*['\"]{2}" src --include=*.ts | head
grep -rnE "outlet:\s*['\"]" src --include=*.ts | head
grep -rn "<router-outlet[^>]*name=" src --include=*.html | head

The second and third commands tell you whether the app uses named outlets at all. That decides which version of the request guard below you can use.

Step 4: check memory limits and logs

Find the heap and memory limits your SSR workers actually run with: the container memory limit, any --max-old-space-size flag in the start command or NODE_OPTIONS, and the memory tier of any serverless function. Then search recent logs for the patterns in the Technical details section, especially out-of-memory exits from SSR processes and 302 responses with Location values that start with two slashes.

Step 5: confirm the redirect fix on your own staging host

After upgrading, you can confirm the redirect behavior changed by sending the advisory's own example path to a staging deployment you control and checking the response headers. Do this only against your own systems.

curl -s -o /dev/null -D - "https://staging.example.internal/.//example.com" | grep -iE '^(HTTP|location)'

On a vulnerable build that has a route shape matching the advisory's preconditions, you would expect a 302 with location: //example.com. On a fixed build, or behind a guard, you should see a 400, a 404 or a redirect that stays on your own host.

What to do now

  1. Update SSR apps on a supported major. Move @angular/router, @angular/platform-server and the rest of the framework packages together, since Angular expects matching versions across @angular/*.

    # Angular 22
    npm install @angular/core@22.2.3 @angular/common@22.2.3 @angular/router@22.2.3 \
      @angular/platform-server@22.2.3 @angular/platform-browser@22.2.3 @angular/compiler@22.2.3
    # Angular 21 (LTS)
    npx ng update @angular/core@21 @angular/cli@21
    # Angular 20 (LTS until 28 Nov 2026)
    npx ng update @angular/core@20 @angular/cli@20
    npm ls @angular/router @angular/platform-server @angular/common
    

    ng update with a major number moves you to the newest release on that major, which on 25 October meant 22.2.3, 21.2.25 and 20.3.34. Check the result with npm ls and commit the lockfile.

  2. Rebuild and redeploy the server bundle. The fix lives in code that is bundled into your server output, so updating the lockfile is not enough. Rebuild, redeploy every SSR environment and confirm the running build's version.

  3. For Angular 20, plan the next major now. 20.3.34 is the release to be on, and the line ends on 28 November 2026.

  4. For Angular 19 and older, add the request guard and schedule the upgrade. ng update refuses to jump more than one major at once; its own error message says "Please migrate each major version individually". So 19 to 21 is two steps: ng update @angular/core@20 @angular/cli@20, test, then ng update @angular/core@21 @angular/cli@21. Treat it as a project, not a patch.

  5. Reject outlet syntax at the edge if your URLs never use named outlets. It is cheap, and the router advisory says it fully blocks both the heap exhaustion and the short-URL guard amplification.

  6. Normalize or reject dot segments and semicolons at the proxy, and refuse protocol-relative Location headers coming from the SSR upstream.

  7. Give SSR workers sane limits and fast restarts, so a crash costs one worker for a moment rather than the whole service.

For the wider hardening picture beyond these two CVEs, see our Angular application security checklist.

A request guard for Node.js until you upgrade

The router advisory recommends rejecting outlet syntax before Angular SSR sees the request, and the platform-server advisory recommends rejecting dot segments followed by slashes. This guard does both. It decodes the path first, so percent-encoded parentheses and dots do not slip through, and it rejects malformed encodings outright. It is plain JavaScript and works as Express middleware or in a plain Node.js handler.

// Stopgap request guard for Angular SSR (CVE-2026-111752 and CVE-2026-111754).
// Follows the workarounds in GHSA-62vg-58rm-qff7 and GHSA-w739-gvwx-grc3.
// Returns a reason string when the request should be rejected, or null.
export function ssrGuardReason(rawUrl, { usesNamedOutlets = false } = {}) {
  const rawPath = rawUrl.split(/[?#]/, 1)[0];
  let path;
  try {
    path = decodeURIComponent(rawPath); // also decodes %2F, %28, %29, %2E, %3B
  } catch {
    return "malformed percent-encoding";
  }
  // CVE-2026-111752: auxiliary outlet groups always use parentheses.
  if (!usesNamedOutlets && /[()]/.test(path)) return "outlet syntax";
  if (usesNamedOutlets && /:\/\(|\(\/|\/\/\/\(/.test(path)) return "empty-path outlet group";
  // CVE-2026-111754: dot-segments followed by extra slashes, or ";" after a dot.
  if (/\/\.{1,2}\/\/|\/\.{1,2};/.test(path)) return "dot-segment before slashes";
  // Belt and braces: a path that already starts with two slashes.
  if (path.startsWith("//")) return "protocol-relative path";
  return null;
}

// Express or Node http style middleware. Place it before the Angular SSR handler.
export function ssrGuard(options) {
  return (req, res, next) => {
    const reason = ssrGuardReason(req.url, options);
    if (reason) {
      res.statusCode = 400;
      res.end("Bad Request");
      return;
    }
    next();
  };
}

In a standard Angular SSR server.ts, register it with app.use(ssrGuard({ usesNamedOutlets: false })) before the line that hands requests to the Angular engine. We tested it with Node.js 26.7.0 against the advisories' example shapes and a few ordinary paths:

$ node test.mjs
allow   outlets:no   /products/42
allow   outlets:no   /products/42?sort=(price)
allow   outlets:no   /search?q=a//b
reject  outlets:no   /shop/(a:/()//b:/())                     outlet syntax
reject  outlets:no   /dashboard/(detail:7)                    outlet syntax
allow   outlets:yes  /dashboard/(detail:7)
reject  outlets:yes  /dashboard/(detail:1//x:/(detail:2))     empty-path outlet group
reject  outlets:yes  /shop///(a//(b))                         empty-path outlet group
reject  outlets:no   /a:/%28x:/%28%29%29                      outlet syntax
reject  outlets:no   /bad%E0%A4%A                             malformed percent-encoding
reject  outlets:no   /.//evil.test                            dot-segment before slashes
reject  outlets:yes  /.;/(//evil.test)                        empty-path outlet group
reject  outlets:no   /%2e//evil.test                          dot-segment before slashes
allow   outlets:no   /docs/v1.2/intro
reject  outlets:no   /files/..//x                             dot-segment before slashes
reject  outlets:no   //evil.test                              protocol-relative path

Two notes on the design. Parentheses in the query string are allowed, because the router only treats them as outlets in the path. And we decode with decodeURIComponent, which is stricter than the decodeURI call in the advisory's own example: it also decodes %2F and %3B, so an encoded slash or semicolon cannot be used to dodge the dot-segment rule. If your app legitimately uses encoded slashes in paths, test before deploying.

The advisory's sample middleware is worth reading but copy it with care. As published on GitHub on 25 October, its "Option B" regular expression contains an unescaped slash inside a JavaScript regex literal, which will not parse as written. The rule it describes, rejecting :/(, (/ and ///(, is the one implemented above.

The same rules at Nginx

If Nginx sits in front of your SSR servers, you can reject the same shapes before they reach Node.js. Nginx's $request_uri is the raw, undecoded request target, so the patterns include the percent-encoded forms:

# Reject outlet syntax in the path (use only if the app has no named outlets in URLs).
if ($request_uri ~* "^[^?]*(\(|\)|%28|%29)") {
    return 400;
}
# Reject dot segments followed by two slashes, or by a semicolon (CVE-2026-111754).
if ($request_uri ~* "^[^?]*/(\.|%2e){1,2}((/|%2f){2}|;|%3b)") {
    return 400;
}

We checked these patterns against the same test paths with a PCRE-compatible engine; we did not run them inside Nginx, so test them on staging first. Keep each if block to a single return, which avoids the surprises if can cause inside a location. The platform-server advisory's own Nginx example, as published, contains a stray \n in its pattern, so use a version you have tested.

Heap limits and restarts

Neither limit fixes the bug, but both change how much one attacker can do. The router advisory says raising --max-old-space-size raises the request size and concurrency needed to exhaust memory without removing the issue. Pair it with a process manager or orchestrator that restarts crashed workers quickly, and run more than one worker so a single crash does not take the site down:

# Example: give the SSR server a larger old-space heap than a tiny container default
NODE_OPTIONS="--max-old-space-size=1024" node dist/my-app/server/server.mjs

A URL length limit at the proxy helps a little. The advisory suggests something like 2,048 bytes for the full request target, but notes that it does not stop the guard and resolver amplification, which needs only a short URL.

Common mistakes

  • Testing with ng serve and seeing nothing. The public issue reports that a development build answered 404 and the worker was unaffected. Production builds behave differently. Test against a production build on staging.
  • Bumping only @angular/router. The redirect fix is in @angular/platform-server. Update the whole framework set together.
  • Updating the lockfile and not redeploying. The fix ships inside the server bundle. Until you rebuild and redeploy, the running server is unchanged.
  • Assuming prerendered means safe. Prerendered routes are safe. Any route that renders on demand, including fallbacks and routes using RenderMode.Server, is not.
  • Checking returnUrl.startsWith("//") and calling it done. The advisory says this exact check accepts /.//evil.test. Validate the normalized URL instead: build it with new URL(input, origin) and compare the resulting origin to your own.
  • Filtering the raw path only. An encoded parenthesis or dot gets past a raw-string filter if anything downstream decodes it. Decode first, and reject malformed encodings.
  • Treating "no CVE.org record" as "not real". The IDs are assigned and the GitHub advisories reviewed. Scanners that rely only on NVD may stay silent for a while.

What changed since 20 October

  • No CVE.org records yet. We rechecked on 25 October. CVE.org's API returns no record for CVE-2026-111751, CVE-2026-111752, CVE-2026-111753 or CVE-2026-111754, so NVD has nothing to score either. The GitHub advisories are still the authoritative public records.
  • Newer patch releases. 20.3.34 and 22.2.3 were already on npm by 14 October and are now the v20-lts and latest tags. 21.2.25 remains the newest 21.x release.
  • A related advisory for Angular 20. CVE-2026-111753 in @angular/common (CVSS 4.0 5.9, moderate in the GitHub Advisory Database, although Angular's own repository advisory labels it high), published by Angular on 16 October and reviewed by GitHub on the evening of 20 October, means Angular 20 apps should be on 20.3.34, not 20.3.33.
  • Fuller preconditions. The router advisory's heap size condition and its Node.js header size note are now reflected in this post, and the redirect advisory's proxy workarounds are included instead of treating the redirect as upgrade-only.
  • Download shares. In the latest week, 25.9% of @angular/router downloads were for Angular 19 and older, down slightly from the 26.5% we measured on 20 October. The share on fixed versions has grown as 21.2.25 and 22.2.x became the most downloaded releases.
  • No exploitation reported. Neither CVE is on CISA's KEV list, and we found no public report of exploitation.

Open questions

  • When will CVE.org publish the records? Until it does, tools that depend on NVD may not flag these versions. We will update this post when records appear.
  • Will NVD score them differently? The only scores today are the CVSS 4.0 scores in the GitHub advisories. An NVD score, if one comes, could differ.
  • What will happen to Angular 20 after 28 November? Angular's policy says support ends. Any SSR advisory after that date will likely read "will not be patched" for 20.x, as it does for 19.x today.

What would have helped

Know which apps render on a server. The single most useful fact here is which deployed Angular apps use SSR. A short inventory with the app, its rendering mode, its Angular major and the owner turns this from a hunt into a list. Our guide to finding affected products fast with SBOMs shows how to build that list before you need it.

Stay inside the support window. These fixes shipped on the same day for every supported line. Teams on Angular 21 or 22 had a one line change. Teams on 19 had a migration project. A rule that no production app runs on a major that ends within three months avoids that position.

Take patch releases routinely. Every project that took a normal patch update in early October was fixed before GitHub's database listed the CVEs.

Validate URLs after normalization, everywhere. The redirect bug is a reminder that string checks on raw URLs are fragile. Any redirect target your own code handles should be parsed with the WHATWG URL parser and compared by origin. Our open redirect explainer covers the patterns in more depth.

Put a normalizing proxy in front of SSR, and supervise the workers. Rejecting dot segments, semicolons and characters your app never uses protects against whole classes of URL parsing bugs. A sized heap, several workers and fast restarts turn a one-request crash into a blip.

Where Safeguard fits

Safeguard's scanner builds SBOMs and dependency inventories for repositories, container images and SBOM files, for example with safeguard scan --dir ., safeguard scan --image <name> or safeguard scan --sbom <file>, and writes results as JSON under ./output. The platform then shows which projects contain a given component and version, such as @angular/router 19.2.25 or @angular/platform-server 21.2.20, and matches components against vulnerability databases as those databases carry these advisories. Reachability analysis helps rank which vulnerable code paths your application actually calls.

Policies and gates evaluated in the platform can stop a build that still resolves an affected version, or one on an unsupported Angular major. When a fixed version exists, autonomous remediation can open fix pull requests that move the framework packages together; it ships off by default.

The Safeguard CLI's assistant can do the hands-on work with your approval: run npm ls across a workspace, apply ng update, add the request guard to server.ts, rebuild and run your tests, asking before any command or file edit that changes something, under the /permissions mode you choose. In CI, safeguard exec runs the same kind of task non-interactively.

Where Safeguard does not help: it does not see your running SSR traffic, cannot block a crafted request at your edge and does not replace the proxy rules above. Dependency data tells you an affected version is present; it does not tell you whether your route configuration and heap size meet the advisory's preconditions. Safeguard would not have prevented these bugs, and it cannot make a fix exist for Angular 19.

Zero-Day Discovery and Gold

Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories. We make no claim that it found either of these bugs or that an SGZ advisory existed for them; both were reported on Angular's public issue tracker and fixed by the Angular team. We make no claim that Gold holds a record for either CVE or a patched Angular 19 build; check gold.safeguard.sh yourself rather than relying on this post. The Gold section below explains what it is for.

FAQ

Which versions fix these CVEs?

20.3.33, 21.2.25 and 22.2.1, for @angular/router (CVE-2026-111752) and @angular/platform-server (CVE-2026-111754). On 25 October the newest releases on each line were 20.3.34, 21.2.25 and 22.2.3, and they include both fixes. Update the whole framework set together.

Is there a fix for Angular 19?

No. Angular 19 and older are out of support, and every advisory in this run says those lines "will not be patched". Use the request guard and proxy rules as a stopgap and move to a supported major.

Why is there a CVE number but no CVE.org page?

The IDs are assigned, and the advisories were published by Angular and reviewed by GitHub, but CVE.org had not published the records when we checked on 25 October. Cite the GHSA IDs, GHSA-62vg-58rm-qff7 and GHSA-w739-gvwx-grc3, until the CVE records appear.

Does a URL length limit stop the crash?

Not fully. The router advisory says a length limit reduces the heap exhaustion risk but does not stop the guard and resolver amplification, which needs only a short URL. Rejecting parentheses or empty-path outlet sequences is the mitigation the advisory says completely blocks both.

We use named outlets in our URLs. Can we still filter?

Yes. Reject only the empty-path outlet sequences, :/(, (/ and ///(, rather than all parentheses. The guard in this post does that when you pass usesNamedOutlets: true.

Is either CVE being exploited?

We found no report of exploitation, and neither is on CISA's KEV list as of 25 October. The router advisory and the public issue do describe the request shapes in detail, so treat the crash as easy to trigger once someone tries.

Does @angular/ssr need updating too?

The fixes are in @angular/router and @angular/platform-server, not @angular/ssr. Keep @angular/ssr current on your major anyway; it has its own advisories, such as CVE-2026-104871 for Windows path traversal in CommonEngine, fixed in 20.3.36, 21.2.23 and 22.1.7.

We are on Angular 20.3.33. Are we done?

Not quite. Move to 20.3.34 for CVE-2026-111753, and plan the move to 21 or 22 before Angular 20's LTS ends on 28 November 2026.

Fix it without waiting for upstream

For supported Angular lines the fix exists and the job is getting it into every SSR build quickly. For Angular 19 and older, upstream has said it will not ship one, which is the second gap below.

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would have helped

None of these demos shows these Angular CVEs. Each shows a real flow on another example, and the notes explain how the same flow applies here.

The CLI demo fixes a lodash CVE: the assistant reads the finding and the advisory, asks before any command that changes something, edits package.json, runs npm install and npm test with your approval, prints a summary and pushes nothing. For these CVEs you would run the same kind of session in your Angular workspace and ask it to move the @angular/* packages to 21.2.25 or 22.2.3, add the request guard to server.ts, and run the production build and tests, approving each step.

Click through it at your own pace, or open it full screen.

The desktop app demo sends work to a computer running the CLI: you pick the folder, then Approve or Deny each command and file edit, and it ends with an upgrade done and tests passing. Here that could be the first step of an Angular 19 migration, ng update @angular/core@20 @angular/cli@20, reviewed one change at a time before the second step to 21.

Click through it at your own pace, or open it full screen.

The Claude demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. With that in place you could ask Claude which of your projects contain @angular/router below 21.2.25, or still on Angular 19, and get the list from your own Safeguard data.

Click through it at your own pace, or open it full screen.

To try it on your own code, download Safeguard and follow the CLI docs.

Quick check

Your Angular 21 app renders entirely in the browser, with no SSR. Do CVE-2026-111752 and CVE-2026-111754 affect it?

No. Both require server-side rendering. The router advisory says client-side apps are not vulnerable, because any memory blowup happens in the visitor's own browser, and the redirect code runs only on the server. Update anyway at your next routine update.

Your SSR app is on Angular 19.2.25. Which version fixes CVE-2026-111752?

None on the 19 line. The fixes are in 20.3.33, 21.2.25 and 22.2.1, and Angular 19 is no longer supported. Upgrade one major at a time to a supported line, and filter outlet syntax in front of SSR until then.

Why does the request guard decode the path before checking it?

Because a parenthesis or a dot can be sent percent-encoded, as %28, %29 or %2e. A filter that checks only the raw path would let the encoded form through if anything downstream decodes it. Rejecting malformed encodings closes the remaining gap.

Sources

Last reviewed: 25 October 2026. This is a developing story; check the linked advisories for the latest status.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.