From 13b6bbd6a02190e7cf87f652b2521b1e462c2474 Mon Sep 17 00:00:00 2001 From: Shuaib Hasan Akib Date: Wed, 8 Jul 2026 23:10:17 +0600 Subject: [PATCH] docs(platform-server): add error reference page for NG05703 and wire up RuntimeError Add a dedicated error reference page for NG05703 (suspicious URL origin change during SSR) and update the error to use RuntimeError with a negative code so the error message automatically includes a link to the docs page in both dev and production builds. Update affected tests in url_spec.ts, platform_location_spec.ts, and integration_spec.ts to match the new NG05703-prefixed error message format. Fixes: #69667 --- adev/src/content/reference/errors/NG05703.md | 25 +++++++++++++++++++ adev/src/content/reference/errors/overview.md | 1 + .../public-api/platform-server/index.api.md | 2 +- packages/platform-server/src/errors.ts | 2 +- .../platform-server/test/integration_spec.ts | 12 +++------ .../test/platform_location_spec.ts | 12 +++------ packages/platform-server/test/url_spec.ts | 12 +++------ 7 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 adev/src/content/reference/errors/NG05703.md diff --git a/adev/src/content/reference/errors/NG05703.md b/adev/src/content/reference/errors/NG05703.md new file mode 100644 index 00000000000..e91fdaafd69 --- /dev/null +++ b/adev/src/content/reference/errors/NG05703.md @@ -0,0 +1,25 @@ +# Suspicious URL Origin Change + +This error occurs in Server-Side Rendering (SSR) environments when a URL resolution results in an unexpected origin change. This check is a security feature in `@angular/platform-server` designed to protect your application from Server-Side Request Forgery (SSRF) and security bypasses. + +## Description + +To perform HTTP requests and process route states during SSR, Angular must resolve relative URLs to absolute URLs. + +During this resolution process, Angular validates that the resolved URL does not unexpectedly point to an external or untrusted origin. If a URL behaves like a relative path but resolves to a different origin, Angular throws `NG05703` and blocks the request/navigation. + +### Common causes + +- **Backslash-prefixed hijack attempts (open redirects / SSRF bypass):** Relative paths beginning with slashes and backslashes (such as `/\\attacker.com/path` or `\\\\attacker.com/path`) can be interpreted differently by browsers and server-side URL parsers. In some parser implementations, these resolve to an absolute URL pointing to `attacker.com`. Angular detects this mismatch and blocks the resolution. +- **Disallowed state changes to different origins:** Navigation or URL updates (e.g. using `location.replaceState` or `location.pushState`) that change the origin of the application when the environment is configured to restrict URL changes to the current origin only. +- **SSR bootstrap URL mismatch:** In server-side rendering, if the `url` passed to the renderer does not align with the base origin configured for the application (such as `APP_BASE_HREF`), the router might attempt to synchronize the location on startup, causing a disallowed origin change. +- **Obfuscated protocol schemes:** Absolute URLs with obfuscated or malformed protocols (e.g. `ht\ntp://evil.com/path`) that attempt to bypass origin checks. + +## Debugging the error + +Check the URL that triggered the error. Ensure it does not contain unexpected backslashes, line breaks, or other characters that could trigger parsing anomalies: + +- Avoid using backslashes (`\`) in relative URLs or routing paths. +- Make sure any user-supplied URLs or query parameters are properly validated and sanitized before being processed in SSR. +- **Align SSR bootstrap URL with configured base URL:** If the error occurs on startup in an SSR environment, ensure that the `url` passed to the renderer matches the expected base origin/host (e.g. `APP_BASE_HREF`). Avoid using dynamic URLs from raw request headers (like `X-Forwarded-Host`) if they do not match the trusted application origin. +- If you intentionally want to make requests to a different domain via a protocol-relative URL (`//other-domain.com/path`), ensure it is allowed by the configuration or use an explicit scheme (`http://` or `https://`). diff --git a/adev/src/content/reference/errors/overview.md b/adev/src/content/reference/errors/overview.md index dbb8b73afb5..6e7bd21c8b9 100644 --- a/adev/src/content/reference/errors/overview.md +++ b/adev/src/content/reference/errors/overview.md @@ -46,6 +46,7 @@ | `NG02825` | [Fetch response body exceeds the configured limit](errors/NG02825) | | `NG05000` | [Hydration with unsupported Zone.js instance.](errors/NG05000) | | `NG05104` | [Root element was not found.](errors/NG05104) | +| `NG05703` | [Suspicious URL origin change during SSR](errors/NG05703) | ## Compiler errors diff --git a/goldens/public-api/platform-server/index.api.md b/goldens/public-api/platform-server/index.api.md index 984923c3042..13cc041cdfd 100644 --- a/goldens/public-api/platform-server/index.api.md +++ b/goldens/public-api/platform-server/index.api.md @@ -76,7 +76,7 @@ export const enum RuntimeErrorCode { // (undocumented) PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 5702, // (undocumented) - SUSPICIOUS_URL_CHANGE_ORIGIN = 5703, + SUSPICIOUS_URL_CHANGE_ORIGIN = -5703, // (undocumented) XHR_NOT_LOADED = 5705 } diff --git a/packages/platform-server/src/errors.ts b/packages/platform-server/src/errors.ts index 68bb0d768f3..2d4d4d55458 100644 --- a/packages/platform-server/src/errors.ts +++ b/packages/platform-server/src/errors.ts @@ -14,7 +14,7 @@ export const enum RuntimeErrorCode { GET_COOKIE_NOT_IMPLEMENTED = 5700, INVALID_URL = 5701, PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 5702, - SUSPICIOUS_URL_CHANGE_ORIGIN = 5703, + SUSPICIOUS_URL_CHANGE_ORIGIN = -5703, DISABLED_DOM_EMULATION_IN_NON_BROWSER = 5704, XHR_NOT_LOADED = 5705, HOST_NOT_ALLOWED = 5706, diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index e4b7550f1c0..ace92bf8ca2 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -1470,9 +1470,7 @@ class HiddenModule {} http.get('/\\evil.com/api').subscribe({ next: () => fail('Expected request to fail, but it succeeded.'), error: (err) => { - expect(err.message).toBe( - `NG05703: URL /\\evil.com/api changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, - ); + expect(err.message).toMatch(/NG05703/); }, }); @@ -1597,9 +1595,7 @@ class HiddenModule {} http.get(badUrl).subscribe({ 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.`, - ); + expect(err.message).toMatch(/NG05703/); }, }); } @@ -1620,9 +1616,7 @@ class HiddenModule {} http.get(badUrl).subscribe({ 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.`, - ); + expect(err.message).toMatch(/NG05703/); }, }); } diff --git a/packages/platform-server/test/platform_location_spec.ts b/packages/platform-server/test/platform_location_spec.ts index da1a1986bc9..6f4426ffb02 100644 --- a/packages/platform-server/test/platform_location_spec.ts +++ b/packages/platform-server/test/platform_location_spec.ts @@ -146,9 +146,7 @@ import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; }, ]); - expect(() => platform.injector.get(DOCUMENT)).toThrowError( - `NG05703: URL /\\attacker.com/deep/path changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, - ); + expect(() => platform.injector.get(DOCUMENT)).toThrowError(/NG05703/); platform.destroy(); }); @@ -163,9 +161,7 @@ import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; }, ]); - expect(() => platform.injector.get(DOCUMENT)).toThrowError( - `NG05702: Protocol relative URLs are not allowed in this context. URL: //attacker.com/deep/path`, - ); + expect(() => platform.injector.get(DOCUMENT)).toThrowError(/NG05702/); platform.destroy(); }); @@ -182,7 +178,7 @@ import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; const location = platform.injector.get(PlatformLocation); expect(() => location.replaceState(null, 'Title', 'http://attacker.com/foo')).toThrowError( - `NG05703: URL http://attacker.com/foo changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + /NG05703/, ); platform.destroy(); }); @@ -200,7 +196,7 @@ import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; const location = platform.injector.get(PlatformLocation); expect(() => location.pushState(null, 'Title', 'http://attacker.com/foo')).toThrowError( - `NG05703: URL http://attacker.com/foo changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + /NG05703/, ); platform.destroy(); }); diff --git a/packages/platform-server/test/url_spec.ts b/packages/platform-server/test/url_spec.ts index 878a659d371..387f011a8df 100644 --- a/packages/platform-server/test/url_spec.ts +++ b/packages/platform-server/test/url_spec.ts @@ -20,9 +20,7 @@ describe('resolveUrl', () => { it('should throw on backslash-prefixed hijack attempts', () => { const urls = ['/\\attacker.com/deep/path', '\\\\attacker.com/deep/path']; for (const url of urls) { - expect(() => resolveUrl(url, 'http://test.com')).toThrowError( - `NG05703: URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, - ); + expect(() => resolveUrl(url, 'http://test.com')).toThrowError(/NG05703/); } }); @@ -35,9 +33,7 @@ describe('resolveUrl', () => { it('should throw when allowOriginChange is false and origin changes', () => { expect(() => resolveUrl('http://other.com/deep/path', 'http://test.com', {allowOriginChange: false}), - ).toThrowError( - `NG05703: URL http://other.com/deep/path changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, - ); + ).toThrowError(/NG05703/); }); it('should resolve same origin when allowOriginChange is false', () => { @@ -70,9 +66,7 @@ describe('resolveUrl', () => { it('should throw on obfuscated protocols attempting to change origin', () => { const url = 'ht\ntp://evil.com/path'; - expect(() => resolveUrl(url, 'http://test.com')).toThrowError( - `NG05703: URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, - ); + expect(() => resolveUrl(url, 'http://test.com')).toThrowError(/NG05703/); }); });