Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions packages/platform-server/src/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export function resolveUrl(
const {allowProtocolRelative = false, allowOriginChange = true} = options;

if (resolved) {
if (isDisallowedProtocolRelative(resolved, allowProtocolRelative)) {
throwProtocolRelativeUrlError(urlStr);
}

if (originUrl && !isSafeOriginChange(resolved, originUrl, urlStr, allowOriginChange)) {
throwSuspiciousUrlError(urlStr);
}
Expand Down Expand Up @@ -102,26 +106,48 @@ export function resolveUrl(
// and we are configured to allow and preserve standard cross-origin protocol-relative requests.
if (urlStr.startsWith('//')) {
if (!allowProtocolRelative) {
throw new RuntimeError(
RuntimeErrorCode.PROTOCOL_RELATIVE_URL_NOT_ALLOWED,
typeof ngDevMode === 'undefined' || ngDevMode
? `Protocol relative URLs are not allowed in this context. URL: ${urlStr}`
: urlStr,
);
throwProtocolRelativeUrlError(urlStr);
}

return new URL(urlStr, origin);
}

resolved = new URL(urlStr, origin);

if (isDisallowedProtocolRelative(resolved, allowProtocolRelative)) {
throwProtocolRelativeUrlError(urlStr);
}

if (!isSafeOriginChange(resolved, originUrl, urlStr, allowOriginChange)) {
throwSuspiciousUrlError(urlStr);
}

return resolved;
}

/**
* Checks if the resolved URL has a disallowed protocol-relative path.
*
* @param resolved The resolved URL.
* @param allowProtocolRelative Whether protocol-relative URLs are allowed.
* @returns True if the URL has a disallowed protocol-relative path, false otherwise.
*/
function isDisallowedProtocolRelative(resolved: URL, allowProtocolRelative: boolean): boolean {
return !allowProtocolRelative && resolved.pathname.startsWith('//');
}

/**
* Throws a protocol-relative URL error indicating that protocol-relative URLs are not allowed.
*/
function throwProtocolRelativeUrlError(urlStr: string): never {
throw new RuntimeError(
RuntimeErrorCode.PROTOCOL_RELATIVE_URL_NOT_ALLOWED,
typeof ngDevMode === 'undefined' || ngDevMode
? `Protocol relative URLs are not allowed in this context. URL: ${urlStr}`
: urlStr,
);
}

/**
* Throws a suspicious URL error indicating a security bypass attempt.
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/platform-server/test/url_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,34 @@ describe('resolveUrl', () => {
expect(() => resolveUrl(url, 'http://test.com')).toThrowError(/NG05703/);
});

it('should throw on protocol-relative URLs when allowProtocolRelative is false or default', () => {
const urls = [
'//attacker.example/collect',
'///attacker.example/collect',
'/.//attacker.example/collect',
'/..//attacker.example/collect',
'/.\\/attacker.example/collect',
'http://test.com/.//attacker.example/collect',
];

for (const url of urls) {
expect(() => resolveUrl(url, 'http://test.com')).toThrowError(/NG05702/);
}
});

it('should allow protocol-relative URLs when allowProtocolRelative is true', () => {
const url = resolveUrl('//attacker.example/collect', 'http://test.com', {
allowProtocolRelative: true,
});
expect(url.href).toBe('http://attacker.example/collect');
expect(url.origin).toBe('http://attacker.example');

const dotUrl = resolveUrl('/.//attacker.example/collect', 'http://test.com', {
allowProtocolRelative: true,
});
expect(dotUrl.href).toBe('http://test.com//attacker.example/collect');
});

it('should not trim unicode whitespace into protocol-relative URLs', () => {
const urls = ['\u00A0//attacker.example/collect', '\uFEFF//attacker.example/collect'];

Expand Down
Loading