From d4f07788b7fd2f4240fa592bfdf2f6808d5ef5c2 Mon Sep 17 00:00:00 2001 From: carrerasdarren-cell <283775510+carrerasdarren-cell@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:15:50 -0400 Subject: [PATCH] fix(router): prevent protocol-relative serialized URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normalize empty leading path segments when serializing UrlTrees so a RouterLink href cannot be interpreted as a scheme-relative external URL. This matches the URL parser’s existing leading-slash normalization. Fixes #69700 --- packages/router/src/url_tree.ts | 3 ++- packages/router/test/create_url_tree.spec.ts | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index ee6853cc0872..3a9e62175d28 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -453,7 +453,8 @@ export class DefaultUrlSerializer implements UrlSerializer { /** Converts a `UrlTree` into a url */ serialize(tree: UrlTree): string { - const segment = `/${serializeSegment(tree.root, true)}`; + // Empty leading path segments would make the result protocol-relative in browsers. + const segment = `/${serializeSegment(tree.root, true).replace(/^\/+/, '')}`; const query = serializeQueryParams(tree.queryParams); const fragment = typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : ''; diff --git a/packages/router/test/create_url_tree.spec.ts b/packages/router/test/create_url_tree.spec.ts index 8132136d1ed5..668c3fe4902f 100644 --- a/packages/router/test/create_url_tree.spec.ts +++ b/packages/router/test/create_url_tree.spec.ts @@ -107,6 +107,12 @@ describe('createUrlTree', () => { expect(serializer.serialize(t)).toEqual('/'); }); + it('should not serialize leading empty path commands as a protocol-relative URL', async () => { + const p = serializer.parse('/'); + const t = await createRoot(p, ['/', '', 'attacker.example', 'collect'], {session: 'secret'}); + expect(serializer.serialize(t)).toEqual('/attacker.example/collect?session=secret'); + }); + it('should error when navigating to the root segment with params', async () => { const p = serializer.parse('/'); await expectAsync(createRoot(p, ['/', {p: 11}])).toBeRejectedWithError(