From 973d42a53ac9346a738d14682909134c9d60445c Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Thu, 28 May 2026 00:19:23 +0530 Subject: [PATCH] fix(platform-server): prevent SSRF bypass via obfuscated URL schemes Embedded tabs/newlines hide a scheme from the relative-URL guard, letting the WHATWG URL parser re-resolve the value to a remote origin during SSR. Prefix a single slash so the forced value can only stay a same-origin path. --- packages/platform-server/src/http.ts | 5 ++++- packages/platform-server/test/integration_spec.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/platform-server/src/http.ts b/packages/platform-server/src/http.ts index e6381b1a9f9..0401639829e 100644 --- a/packages/platform-server/src/http.ts +++ b/packages/platform-server/src/http.ts @@ -79,7 +79,10 @@ function relativeUrlsTransformerInterceptorFn( const isProtocolRelative = /^\/\/[^/\\]/.test(trimmedUrl); if (!isProtocolRelative) { // Unrecognized structure that changed origin. Force it to be a local path. - parsedUrl = new URL(trimmedUrl.replace(/^[/\\]+/, '/'), baseUrl); + // A single leading slash is prepended so the value resolves as an absolute path + // and cannot be reinterpreted as an absolute URL, e.g. when the scheme is hidden + // with embedded tabs/newlines (`ht\ttp://attacker.com`) that the URL parser strips. + parsedUrl = new URL('/' + trimmedUrl.replace(/^[/\\]+/, ''), baseUrl); } } diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 789ef69683d..3c8cd01595c 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -1598,6 +1598,21 @@ class HiddenModule {} }); }); + it('should treat scheme-obfuscated SSRF attempts as pathnames', async () => { + // The URL parser strips embedded tabs/newlines, so these resolve to an absolute + // (cross-origin) URL even though the leading characters do not look like a scheme. + const badUrls = ['ht\ttp://attacker.com', 'ht\ntp://attacker.com']; + + ref.injector.get(NgZone).run(() => { + for (const badUrl of badUrls) { + http.get(badUrl).subscribe((body) => { + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost:4000/http://attacker.com').flush('success!'); + } + }); + }); + it('should resolve safe path-relative URLs containing backslashes without origin change', async () => { ref.injector.get(NgZone).run(() => { http.get('\\testing').subscribe((body) => {