Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Angular SSR pathname normalization can turn a same-origin navigation into an open redirect

Aperta
#34,207 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
52/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
angular, typescript
Ambito
backend, security

Direzione di ricerca

Reproduce the redirect with the AngularNodeAppEngine setup and curl commands in the issue, then inspect the @angular/ssr redirect assembly and ServerPlatformLocation.replaceState() entry points. Compare the normalized values used for the final-URL check with the pathname emitted in the redirect. Done means the supplied parameter-route cases no longer produce a cross-origin Location while same-origin redirects continue to work.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Description

A route whose path ends in a parameter and has children is enough for AngularNodeAppEngine to return a cross-origin redirect from the request path alone — no application redirectTo, no guard, no resolver, no X-Forwarded-* control, no authentication:

GET /.;/(//evil.test)   ->   302 Location: //evil.test

A browser resolves //evil.test as protocol-relative and leaves the application's origin.

The Router serializes the navigation to /.//evil.test. That string starts with /., not //, and resolves to the application's own origin, so neither guard in ServerPlatformLocation.replaceState() fires , but WHATWG normalization pops the preceding segment, leaving pathname === '//evil.test'.
@angular/ssr then emits that pathname as the redirect target without normalizing it:

const { pathname, search, hash } = envInjector.get(PlatformLocation);

if (urlToRenderString !== finalUrl) {
  redirectTo = [pathname, search, hash].join("");
}

A pathname may legally begin with //, so nothing upstream is malformed. The comparison arm normalizes its inputs; the redirect arm does not.

Minimal Reproduction
import { Component } from "@angular/core";
import { RouterOutlet, Routes } from "@angular/router";

@Component({ imports: [RouterOutlet], template: "<router-outlet />" })
export class TenantLayout {}

@Component({ template: "tenant page" })
export class TenantPage {}

export const routes: Routes = [
  {
    path: ":tenant",
    component: TenantLayout,
    children: [{ path: "**", component: TenantPage }],
  },
];
npm run build
NG_ALLOWED_HOSTS=localhost PORT=4000 node dist/repro/server/server.mjs
for p in '/.;/(//evil.test)' '/xx;/(//evil.test)' '/acme'; do
  printf '%-22s ' "$p"
  curl -s -o /dev/null -w '%{http_code}  %{redirect_url}\n' --path-as-is "http://localhost:4000$p"
done
/.;/(//evil.test)      302  http://evil.test/
/xx;/(//evil.test)     302  http://localhost:4000/xx//evil.test
/acme                  200

The second line is the control: . replaced with xx removes the dot-segment pop and the redirect stays on the origin. /.;/(/evil.test) gives location: /evil.test, also same-origin , both the popping dot segment and the leading empty segment are required.

Minimal Reproduction

See https://github.com/SkyZeroZx/angular-ssr-router-open-redirect

Your Environment
22.2.0
Anything else relevant?

The canonical application-side mitigation does not stop it. A returnUrl check requiring a relative, non-protocol-relative path accepts /.;/(//evil.test), so an application that validated correctly still redirects off-origin:

export const returnUrlGuard = (route: ActivatedRouteSnapshot) => {
  const target = route.queryParamMap.get("returnUrl") ?? "/";
  if (!target.startsWith("/") || target.startsWith("//")) {
    return true;
  }
  return inject(Router).parseUrl(target);
};

With { path: "login", component: Login, canActivate: [returnUrlGuard] } added to the config above:

/login?returnUrl=%2F.%3B%2F(%2F%2Fevil.test)   302  location: //evil.test   <- accepted
/login?returnUrl=https%3A%2F%2Fevil.test       200  no redirect             <- rejected
/login?returnUrl=%2F%2Fevil.test               200  no redirect             <- rejected

router.navigateByUrl(target) behaves the same. This moves the payload into a query parameter, so the delivered link is /login?returnUrl=... rather than a visibly odd path. search is concatenated unchanged, so query parameters reach the redirect target.

Also reachable without a literal . or // in the request: GET /%2e;/(/\evil.test) returns location: //evil.test, since %2e is decoded and \ normalized after any inspection of the raw path.

Affected shapes: a parameter route with children, plain or lazy, whose descendants can match a two-segment group, at URL depth ≤ 2. Childless parameter routes, children without a **, a ** child using redirectTo.

Lingua principale
TypeScript
Stelle
27k
Fork
11.8k
Merge medio
22h 44m
PR unite (30g)
177

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di angular/angular-cli

Tutte le issue di angular/angular-cli

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.