From 9339a7a2de437ed93f9cc3da7f32d0100412d599 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 25 Aug 2026 11:27:58 +0000 Subject: [PATCH] fix(platform-server): avoid stripping unicode whitespace during url resolution Avoid trimming urlStr with String.prototype.trim() in parseUrl to ensure URL parsing and resolution align with the WHATWG URL standard. --- packages/platform-server/src/url.ts | 2 -- .../platform-server/test/integration_spec.ts | 15 +++++++++++++-- packages/platform-server/test/url_spec.ts | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/platform-server/src/url.ts b/packages/platform-server/src/url.ts index c3afb81d8f1..31d9842469e 100644 --- a/packages/platform-server/src/url.ts +++ b/packages/platform-server/src/url.ts @@ -54,8 +54,6 @@ export function parseUrl( return originUrl || null; } - urlStr = urlStr.trim(); - // Fast-path: if the URL is a valid, standard absolute URL, parse and return it immediately. let resolved: URL | undefined; try { diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 253a6bf1cdc..d46fafd3300 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -1578,6 +1578,17 @@ class HiddenModule {} }); }); + it('should resolve non-breaking space prefixed URLs as relative paths on the same origin', async () => { + ref.injector.get(NgZone).run(() => { + http.get('\u00A0//attacker.example/collect').subscribe((body) => { + expect(body).toEqual('success!'); + }); + mock + .expectOne('http://localhost:4000/%C2%A0//attacker.example/collect') + .flush('success!'); + }); + }); + it('should reject backslash bypass SSRF attempts in relative requests and throw a suspicious origin error', async () => { const badUrls = [ '/\\attacker.com', @@ -1592,7 +1603,7 @@ class HiddenModule {} next: () => fail(`Expected request for ${badUrl} to fail, but it succeeded.`), error: (err) => { expect(err.message).toBe( - `NG05703: URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL ${badUrl} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }, }); @@ -1615,7 +1626,7 @@ class HiddenModule {} next: () => fail(`Expected request for ${badUrl} to fail, but it succeeded.`), error: (err) => { expect(err.message).toBe( - `NG05703: URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL ${badUrl} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }, }); diff --git a/packages/platform-server/test/url_spec.ts b/packages/platform-server/test/url_spec.ts index 09c40ca61c4..9d8d7531c86 100644 --- a/packages/platform-server/test/url_spec.ts +++ b/packages/platform-server/test/url_spec.ts @@ -74,12 +74,29 @@ describe('parseUrl', () => { `NG05703: URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }); + + it('should not trim unicode whitespace into protocol-relative URLs', () => { + const urls = ['\u00A0//attacker.example/collect', '\uFEFF//attacker.example/collect']; + + for (const urlStr of urls) { + const urlWithProtocolRelative = parseUrl(urlStr, 'http://test.com', { + allowProtocolRelative: true, + }); + expect(urlWithProtocolRelative.origin).toBe('http://test.com'); + expect(urlWithProtocolRelative.pathname).toContain('//attacker.example/collect'); + + const urlWithoutProtocolRelative = parseUrl(urlStr, 'http://test.com'); + expect(urlWithoutProtocolRelative.origin).toBe('http://test.com'); + expect(urlWithoutProtocolRelative.pathname).toContain('//attacker.example/collect'); + } + }); }); describe('without origin', () => { it('should return null for relative paths', () => { expect(parseUrl('/deep/path?query#hash')).toBeNull(); expect(parseUrl('deep/path')).toBeNull(); + expect(parseUrl('\u00A0//attacker.com/deep/path')).toBeNull(); }); it('should parse valid absolute URLs', () => {