Angular SSR route-policy confusion can expose client-only data under public cache headers #33555
Description
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:
/profile;
/profile//public
In both cases, a route configured as RenderMode.Client can be unexpectedly rendered on the server while inheriting public cache headers from another ServerRoute.
Minimal Reproduction
Minimal configuration
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: 'profile',
renderMode: RenderMode.Client,
headers: {
'Cache-Control': 'private, no-store',
},
},
{
path: 'profile/public',
renderMode: RenderMode.Server,
headers: {
'Cache-Control': 'public, max-age=300',
},
},
{
path: '**',
renderMode: RenderMode.Server,
headers: {
'Cache-Control': 'public, max-age=300',
},
},
];The /profile component reads a benign request-derived marker through the SSR REQUEST token.
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;'curl -i \
-H 'Cookie: session=PRIVATE_VALUE' \
http://localhost:4000/profile//publicActual behavior
The crafted requests can:
- render the
/profilecomponent on the server; - expose request-derived data in the initial HTML;
- apply
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.Client should not be server-rendered through an alternative URL representation.
Your Environment
Angular 22.X
Anything else relevant?
This was previously reported at https://issuetracker.google.com/u/1/issues/518988455
Activity
glivter commentedon Sep 14, 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():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)):/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:
Line 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.fix(@angular/ssr): resolve the request path through the router's URL …