Redirect parameters (returnUrl, next, redirectTo) are a classic open-redirect vector: an attacker crafts a login link whose returnUrl points to an external phishing domain, and if the app blindly navigates there after authentication, users are silently handed off. This service enforces an allowlist by prefix, explicitly rejects protocol-relative URLs (//evil.com, which browsers treat as same-scheme absolute) and any URL carrying a scheme, and falls back to a safe default whenever the input fails validation or even fails to URI-decode.
import { Injectable, inject } from '@angular/core';import { Router, UrlTree } from '@angular/router';const ALLOWED_INTERNAL_PREFIX = '/app/';@Injectable({ providedIn: 'root' })class SafeRedirectService {private router = inject(Router);resolveSafeTarget(rawReturnUrl: string | null): UrlTree {if (!rawReturnUrl) {return this.router.parseUrl('/app/home');}let decoded: string;try {decoded = decodeURIComponent(rawReturnUrl);} catch {return this.router.parseUrl('/app/home');}const isProtocolRelative = decoded.startsWith('//');const isAbsoluteUrl = /^[a-z][a-z0-9+.-]*:/i.test(decoded);const isInternal = decoded.startsWith(ALLOWED_INTERNAL_PREFIX);if (isProtocolRelative || isAbsoluteUrl || !isInternal) {return this.router.parseUrl('/app/home');}return this.router.parseUrl(decoded);}}