Repository navigation
Angular SSR route-policy confusion can expose client-only data under public cache headers #33555
Description
Activity
- addedgemini-triagedLabel noting that an issue has been triaged by geminiLabel noting that an issue has been triaged by gemini
on Jul 12, 2026 - addedneeds: repro stepsWe cannot reproduce the issue with the information givenWe cannot reproduce the issue with the information givenand removedneeds: repro stepsWe cannot reproduce the issue with the information givenWe cannot reproduce the issue with the information given
on Jul 13, 2026 Confirming this independently, and apologising for the duplicate: I filed #34090 two months after this and my prior-art sweep missed it. A single search for "route-policy" finds this issue immediately. I had searched my own vocabulary, "route-tree tokenisation" and "matcher divergence", rather than the words a reporter would use. That is on me, and I am closing #34090 in favour of this one.
Posting the additional coverage here so it is not lost, since some of it is not in the original report.
The other grammar has a name. The divergence is
RouteTree.getPathSegments():private getPathSegments(route: string): string[] { return route.split('/').filter(Boolean).map(decodeURIComponent); }
against
@angular/router'sDefaultUrlSerializer, in which(,),;and//are metacharacters and unparseable input is silently discarded. Verified against the published@angular/router22.1.6,serialize(parse(x)):input router resolves to /page)/page/page(/page/page;/page/(page)/page/a/1//b/a/1/a/b)c/d/a/bParentheses are part of the class. This issue names
;and//.(and)do the same thing, and/(profile)is a valid spelling of/profilebecause angular/angular#64507 deliberately made an unnamed(...)group mean the primary outlet. Enumerated over printable ASCII, the single-character triggers are exactly(,)and;. A differential sweep of 1575 paths produced 80 divergent paths in two root shapes.The attacker picks which route's policy to borrow, not just the catch-all. The mangled segment list is matched normally, so any route pattern that fits it is selectable. With a long-lived public sub-route under a private prefix, which applications routinely add for share links and print views:
/priv/1 Cache-Control=no-store, private rendered=priv/:id /priv/1/share Cache-Control=public, max-age=31536000, immutable rendered=priv/:id/share /priv/1//share Cache-Control=public, max-age=31536000, immutable rendered=priv/:idLine three is the account page under a year-long immutable label.
Content-Typeand status are attacker-selected too, becausehandleRenderingspreads the matched route'sheadersafterContent-Typeand takesstatusfrom the matched route.What is NOT affected:
canActivateand the other router guards still run, because@angular/routersees an ordinary path. Not an authorization bypass.There is a fix in #34091, which normalises the pathname through the router's own serializer in
ServerRouter.matchbefore tokenising, so both matchers agree. It covers;,//and the parentheses in one place rather than case by case, the waystripMatrixParamshandled the matrix-parameter symptom in 85c18b4. Tests cover the divergent spellings and were confirmed to fail before the change. I will repoint it at this issue.- added a commit that references this issue
on Sep 14, 2026
Description
Angular SSR can select server-route metadata from one route while Angular Router renders a different route when the request URL contains certain ambiguous path forms.
Example:
In both cases, a route configured as
RenderMode.Clientcan be unexpectedly rendered on the server while inheriting public cache headers from anotherServerRoute.Minimal Reproduction
Minimal configuration
The
/profilecomponent reads a benign request-derived marker through the SSRREQUESTtoken.Steps to reproduce
Request the normal client-only route:
curl -i \ -H 'Cookie: session=PRIVATE_VALUE' \ http://localhost:4000/profileThe initial HTML does not contain the request-derived value.
Request either crafted path:
curl -i \ -H 'Cookie: session=PRIVATE_VALUE' \ http://localhost:4000/profile//publicActual behavior
The crafted requests can:
/profilecomponent on the server;Cache-Control: publicmetadata belonging to another server route.Expected behavior
The route used to select
renderMode, status, and response headers must always correspond to the route whose body is rendered.A route configured as
RenderMode.Clientshould not be server-rendered through an alternative URL representation.Your Environment
Anything else relevant?
This was previously reported at https://issuetracker.google.com/u/1/issues/518988455