Files
Shuaib Hasan Akib c0c97b5b22 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
(cherry picked from commit 13b6bbd6a0)
2026-07-09 11:56:06 -07:00

106 lines
3.7 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {resolveUrl} from '../src/url';
describe('resolveUrl', () => {
describe('with origin', () => {
it('should resolve relative paths against origin', () => {
const url = resolveUrl('/deep/path?query#hash', 'http://test.com');
expect(url.href).toBe('http://test.com/deep/path?query#hash');
expect(url.search).toBe('?query');
expect(url.hash).toBe('#hash');
});
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/);
}
});
it('should resolve absolute URLs ignoring origin', () => {
const url = resolveUrl('http://other.com/deep/path', 'http://test.com');
expect(url.href).toBe('http://other.com/deep/path');
expect(url.origin).toBe('http://other.com');
});
it('should throw when allowOriginChange is false and origin changes', () => {
expect(() =>
resolveUrl('http://other.com/deep/path', 'http://test.com', {allowOriginChange: false}),
).toThrowError(/NG05703/);
});
it('should resolve same origin when allowOriginChange is false', () => {
const url = resolveUrl('http://test.com/other-path', 'http://test.com', {
allowOriginChange: false,
});
expect(url.href).toBe('http://test.com/other-path');
});
it('should resolve relative paths when allowOriginChange is false', () => {
const url = resolveUrl('/other-path', 'http://test.com', {allowOriginChange: false});
expect(url.href).toBe('http://test.com/other-path');
});
it('should throw an error for malformed absolute URLs', () => {
const malformedUrls = [
'http://evil.com:80:80/path',
'https://evil.com:80:80/path',
'http://[google.com]/path',
'http://google.com:port/path',
'http://google.com:80a/path',
];
for (const url of malformedUrls) {
expect(() => resolveUrl(url, 'http://test.com')).toThrowError(
new RegExp(`Invalid URL: ${url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`),
);
}
});
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/);
});
});
describe('without origin', () => {
it('should return null for relative paths', () => {
expect(resolveUrl('/deep/path?query#hash')).toBeNull();
expect(resolveUrl('deep/path')).toBeNull();
expect(resolveUrl('/\\attacker.com/deep/path')).toBeNull();
expect(resolveUrl('\\\\attacker.com/deep/path')).toBeNull();
});
it('should parse valid absolute URLs', () => {
const url = resolveUrl('http://other.com/deep/path');
expect(url).not.toBeNull();
expect(url!.href).toBe('http://other.com/deep/path');
expect(url!.origin).toBe('http://other.com');
});
it('should throw an error for malformed absolute URLs', () => {
const malformedUrls = [
'http://evil.com:80:80/path',
'https://evil.com:80:80/path',
'http://[google.com]/path',
'http://google.com:port/path',
'http://google.com:80a/path',
'ht\ntp://evil.com:80:80/path',
];
for (const url of malformedUrls) {
expect(() => resolveUrl(url)).toThrowError(
new RegExp(`Invalid URL: ${url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`),
);
}
});
});
});