Files
Alan Agius 49a60f6045 fix(platform-server): secure location and document initialization against SSRF and path hijack
Normalizes the URL and path parsing logic inside platform-server by consolidating security checks and normalizations into a single, unified parseUrl helper function.

This includes:
- Collapsing multiple consecutive leading slashes and backslashes (e.g., // or /\) to a single forward slash to avoid protocol-relative parsing of path-like & relative inputs.
- Rejecting malformed absolute URLs that are otherwise accepted by lenient DOM parsers like Domino but rejected by standard WHATWG parsers, preventing SSRF / allowedHosts validation bypasses.
- Ensuring parseDocument gets the fully parsed and normalized URL instead of raw, unvalidated configuration values, preventing virtual document hostname adoption/origin hijack.
- Moving parseUrl unit tests into a dedicated url_spec.ts test file to keep platform_location_spec.ts clean and decoupled.
2026-05-27 10:27:30 -07:00

142 lines
4.1 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 {destroyPlatform} from '@angular/core';
import {renderApplication, renderModule} from '@angular/platform-server';
import {isHostAllowed} from '../src/utils';
describe('isHostAllowed', () => {
it('allows matching hostname when in allowedHosts list', () => {
expect(isHostAllowed('test.com', new Set(['test.com', 'example.com']))).toBeTrue();
});
it('allows matching hostname when wildcard matches', () => {
expect(isHostAllowed('sub.example.com', new Set(['test.com', '*.example.com']))).toBeTrue();
});
it('rejects hostname when not in allowedHosts list', () => {
expect(isHostAllowed('evil.com', new Set(['test.com', '*.example.com']))).toBeFalse();
});
it('allows all hostnames when * is in allowedHosts list', () => {
expect(isHostAllowed('anydomain.com', new Set(['*']))).toBeTrue();
});
});
describe('allowedHosts validation in renderApplication', () => {
const bootstrap = (async () => {}) as any;
beforeEach(() => {
destroyPlatform();
});
afterEach(() => {
destroyPlatform();
});
it('should throw an error on bootstrap if host is not allowed', async () => {
await expectAsync(
renderApplication(bootstrap, {
document: '<app></app>',
url: 'http://evil.com/deep/path',
allowedHosts: ['test.com', '*.example.com'],
}),
).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
});
it('should not throw a host validation error on bootstrap if host is allowed', async () => {
try {
await renderApplication(bootstrap, {
document: '<app></app>',
url: 'http://test.com/deep/path',
allowedHosts: ['test.com', '*.example.com'],
});
} catch (error: any) {
expect(error.message).not.toContain('is not allowed');
}
});
it('should throw an error for malformed absolute URLs (SSRF bypass attempt)', async () => {
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) {
await expectAsync(
renderApplication(bootstrap, {
document: '<app></app>',
url,
allowedHosts: ['test.com'],
}),
)
.withContext(`URL: ${url}`)
.toBeRejectedWithError(new RegExp(/Invalid URL:.+/));
}
});
});
describe('allowedHosts validation in renderModule', () => {
class MockModule {}
beforeEach(() => {
destroyPlatform();
});
afterEach(() => {
destroyPlatform();
});
it('should throw an error if host is not allowed', async () => {
await expectAsync(
renderModule(MockModule, {
document: '<app></app>',
url: 'http://evil.com/deep/path',
allowedHosts: ['test.com', '*.example.com'],
}),
).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
});
it('should not throw a host validation error if host is allowed', async () => {
try {
await renderModule(MockModule, {
document: '<app></app>',
url: 'http://test.com/deep/path',
allowedHosts: ['test.com', '*.example.com'],
});
} catch (error: any) {
expect(error.message).not.toContain('is not allowed');
}
});
it('should throw an error for malformed absolute URLs (SSRF bypass attempt)', async () => {
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) {
await expectAsync(
renderModule(MockModule, {
document: '<app></app>',
url,
allowedHosts: ['test.com'],
}),
)
.withContext(`URL: ${url}`)
.toBeRejectedWithError(new RegExp(/Invalid URL:.+/));
}
});
});