javascript / expert
Snippet
Sanitizing User-Supplied Redirect URLs Before Angular Router Navigation
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.
snippet.js
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);}}
angular
Breakdown
1
const ALLOWED_INTERNAL_PREFIX = '/app/';
An explicit allowlist prefix is far safer than a denylist of known-bad patterns, which attackers can bypass with encoding tricks.
2
decoded = decodeURIComponent(rawReturnUrl);
Decoding before validation prevents encoded bypasses like %2F%2Fevil.com from slipping past a naive string check.
3
const isProtocolRelative = decoded.startsWith('//');
Protocol-relative URLs are absolute in browsers despite lacking a scheme, so they must be checked separately from the scheme regex.
4
const isAbsoluteUrl = /^[a-z][a-z0-9+.-]*:/i.test(decoded);
This regex matches the RFC 3986 scheme grammar to catch javascript:, https:, and any other scheme-prefixed payload.
5
return this.router.parseUrl('/app/home');
Every rejection path converges on the same safe UrlTree rather than throwing, keeping the caller's navigation logic simple.