Revert "fix(router): limit protocol-relative URL handling to serialization"

This reverts commit 435f8b2b8b.

(cherry picked from commit 292991e2df)
This commit is contained in:
Alon Mishne
2026-08-17 23:55:32 +00:00
committed by Jessica Janiuk
parent f61f2d0500
commit 223f25ff37
6 changed files with 0 additions and 118 deletions
-2
View File
@@ -37,8 +37,6 @@ export const enum RuntimeErrorCode {
// (undocumented)
OUTLET_NOT_ACTIVATED = 4012,
// (undocumented)
PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 4019,
// (undocumented)
ROOT_SEGMENT_MATRIX_PARAMS = 4003,
// (undocumented)
TWO_SEGMENTS_WITH_SAME_OUTLET = 4006,
-1
View File
@@ -29,5 +29,4 @@ export const enum RuntimeErrorCode {
INFINITE_REDIRECT = 4016,
INVALID_ROUTER_LINK_INPUTS = 4017,
ERROR_PARSING_URL = 4018,
PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 4019,
}
-7
View File
@@ -454,13 +454,6 @@ export class DefaultUrlSerializer implements UrlSerializer {
/** Converts a `UrlTree` into a url */
serialize(tree: UrlTree): string {
const segment = `/${serializeSegment(tree.root, true)}`;
if (segment.startsWith('//')) {
throw new RuntimeError(
RuntimeErrorCode.PROTOCOL_RELATIVE_URL_NOT_ALLOWED,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
'Cannot serialize a UrlTree that would produce a protocol-relative URL.',
);
}
const query = serializeQueryParams(tree.queryParams);
const fragment =
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : '';
@@ -22,8 +22,6 @@ import {timeout} from '@angular/private/testing';
describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer();
const protocolRelativeUrlError =
/NG04019: Cannot serialize a UrlTree that would produce a protocol-relative URL/;
let router: Router;
beforeEach(() => {
router = TestBed.inject(Router);
@@ -136,62 +134,6 @@ describe('createUrlTree', () => {
expect(serializer.serialize(t)).toEqual('/%2Fone/two%2Fthree');
});
describe('leading empty path commands', () => {
it('should reject absolute navigations that would serialize as protocol-relative', () => {
const t = router.createUrlTree(['/', '', '', 'attacker.example', 'collect']);
expect(() => serializer.serialize(t)).toThrowError(protocolRelativeUrlError);
});
it('should reject an unsafe primary outlet string', async () => {
await router.navigateByUrl('/safe');
const t = router.createUrlTree([{outlets: {primary: '/attacker.example/collect'}}]);
expect(() => serializer.serialize(t)).toThrowError(protocolRelativeUrlError);
});
it('should reject an unsafe primary outlet array', () => {
const t = router.createUrlTree([{outlets: {primary: ['', 'attacker.example', 'collect']}}]);
expect(() => serializer.serialize(t)).toThrowError(protocolRelativeUrlError);
});
it('should reject unsafe parent-relative commands', async () => {
router.resetConfig([{path: 'source', component: class {}}]);
await router.navigateByUrl('/source');
const t = create(router.routerState.root.firstChild!, [
'../',
'',
'attacker.example',
'collect',
]);
expect(() => serializer.serialize(t)).toThrowError(protocolRelativeUrlError);
});
it('should reject an escaped slash after an empty path command', () => {
const t = router.createUrlTree(['/', '', {segmentPath: '/'}]);
expect(() => serializer.serialize(t)).toThrowError(protocolRelativeUrlError);
});
it('should reject final empty path commands', () => {
const t = router.createUrlTree(['/', '', '']);
expect(() => serializer.serialize(t)).toThrowError(protocolRelativeUrlError);
});
it('should not normalize a leading empty path command in a secondary outlet', () => {
const t = router.createUrlTree(['/', {outlets: {right: ['', 'child']}}]);
expect(t.root.children['right'].segments.map((segment) => segment.path)).toEqual([
'',
'child',
]);
expect(serializer.serialize(t)).toEqual('/(right:/child)');
});
});
describe('named outlets', () => {
it('should preserve secondary segments', async () => {
const p = serializer.parse('/a/11/b(right:c)');
-20
View File
@@ -329,24 +329,4 @@ describe('RouterLink', () => {
await harness.navigateByUrl('/different');
expect(anchor.getAttribute('href')).toBe('/different/child');
});
it('rejects a link that would generate a protocol-relative href', async () => {
@Component({
template: `<a [routerLink]="commands" queryParamsHandling="preserve">commands</a>`,
imports: [RouterLink],
})
class WithLink {
readonly commands = ['/', '', 'attacker.example', 'collect'];
}
TestBed.configureTestingModule({
providers: [provideRouter([{path: '', component: WithLink}])],
});
const fixture = TestBed.createComponent(WithLink);
await expectAsync(fixture.whenStable()).toBeRejectedWithError(
/NG04019: Cannot serialize a UrlTree that would produce a protocol-relative URL/,
);
expect(fixture.nativeElement.querySelector('a').getAttribute('href')).toBeNull();
});
});
@@ -13,14 +13,11 @@ import {
encodeUriQuery,
encodeUriSegment,
serializePath,
UrlSegment,
UrlSegmentGroup,
} from '../src/url_tree';
describe('url serializer', () => {
const url = new DefaultUrlSerializer();
const protocolRelativeUrlError =
/NG04019: Cannot serialize a UrlTree that would produce a protocol-relative URL/;
it('should parse the root url', () => {
const tree = url.parse('/');
@@ -450,33 +447,6 @@ describe('url serializer', () => {
});
});
describe('leading empty path segments', () => {
it('should reject a parsed primary outlet that would serialize as protocol-relative', () => {
const tree = url.parse('/(primary://attacker.example/collect)?token=RESET_TOKEN');
expect(() => url.serialize(tree)).toThrowError(protocolRelativeUrlError);
});
it('should reject multiple leading empty primary segments', () => {
const tree = url.parse('/attacker.example/collect');
tree.root.children[PRIMARY_OUTLET].segments.unshift(
new UrlSegment('', {}),
new UrlSegment('', {}),
);
expect(() => url.serialize(tree)).toThrowError(protocolRelativeUrlError);
});
it('should reject unsafe trees with secondary outlets, query params, and fragments', () => {
const tree = url.parse(
'/attacker.example/collect(popup:compose)?token=RESET_TOKEN#OAUTH_TOKEN',
);
tree.root.children[PRIMARY_OUTLET].segments.unshift(new UrlSegment('', {}));
expect(() => url.serialize(tree)).toThrowError(protocolRelativeUrlError);
});
});
describe('error handling', () => {
it('should throw when invalid characters inside children', () => {
expect(() => url.parse('/one/(left#one)')).toThrowError();