diff --git a/packages/platform-server/src/url.ts b/packages/platform-server/src/url.ts index 8dad4ac18c8..d0639980db8 100644 --- a/packages/platform-server/src/url.ts +++ b/packages/platform-server/src/url.ts @@ -57,8 +57,6 @@ export function resolveUrl( 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 f47c8e4c9d4..6d7ab8cba29 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -1583,6 +1583,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', @@ -1597,7 +1608,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.`, ); }, }); @@ -1620,7 +1631,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 878a659d371..38b09ccc626 100644 --- a/packages/platform-server/test/url_spec.ts +++ b/packages/platform-server/test/url_spec.ts @@ -74,6 +74,22 @@ describe('resolveUrl', () => { `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 = resolveUrl(urlStr, 'http://test.com', { + allowProtocolRelative: true, + }); + expect(urlWithProtocolRelative.origin).toBe('http://test.com'); + expect(urlWithProtocolRelative.pathname).toContain('//attacker.example/collect'); + + const urlWithoutProtocolRelative = resolveUrl(urlStr, 'http://test.com'); + expect(urlWithoutProtocolRelative.origin).toBe('http://test.com'); + expect(urlWithoutProtocolRelative.pathname).toContain('//attacker.example/collect'); + } + }); }); describe('without origin', () => { @@ -82,6 +98,7 @@ describe('resolveUrl', () => { expect(resolveUrl('deep/path')).toBeNull(); expect(resolveUrl('/\\attacker.com/deep/path')).toBeNull(); expect(resolveUrl('\\\\attacker.com/deep/path')).toBeNull(); + expect(resolveUrl('\u00A0//attacker.com/deep/path')).toBeNull(); }); it('should parse valid absolute URLs', () => {