Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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

Ouverte
#34,207 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
52/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active
Stack technique
angular, typescript
Domaine
backend, security

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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.

Langage dominant
TypeScript
Étoiles
27k
Forks
11.8k
Merge moyen
23 h 51 min
PR mergées (30 j)
168

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de angular/angular-cli

Toutes les issues de angular/angular-cli

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.