Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Abierto
#34,207 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
52/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
angular, typescript
Área
backend, security

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
TypeScript
Estrellas
27k
Forks
11.8k
Merge medio
22 h 44 min
PR fusionados (30 d)
177

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de angular/angular-cli

Todos los issues de angular/angular-cli

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.