mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
Matthew Beck
parent
53ca8c25cb
commit
13b6bbd6a0
@@ -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://`).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user