From 29641ba7585346f298f86e7d78c9bd332f346a78 Mon Sep 17 00:00:00 2001 From: arshsmith1 Date: Wed, 24 Jun 2026 22:19:47 +0530 Subject: [PATCH] fix(router): handle outlet named __proto__ in segment group maps Outlet maps are keyed by names read verbatim from the url, so a name like `__proto__` (e.g. `/one(__proto__:two)`) is assigned through the inherited `__proto__` setter instead of creating an outlet. This drops the outlet and mutates the map's prototype, and throws under Node's `--disable-proto=throw`. Build these outlet maps with `Object.create(null)` so `__proto__` is treated as an ordinary key. Covers `parseParens` and `squashSegmentGroup` in url_tree.ts, `createSegmentGroup` in apply_redirects.ts, and `replaceSegment` and `updateSegmentGroupChildren` in create_url_tree.ts. --- packages/router/src/apply_redirects.ts | 3 ++- packages/router/src/create_url_tree.ts | 6 ++++-- packages/router/src/url_tree.ts | 9 +++++++-- packages/router/test/url_serializer.spec.ts | 11 +++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/router/src/apply_redirects.ts b/packages/router/src/apply_redirects.ts index 6741d5f3f6db..e63b440570ba 100644 --- a/packages/router/src/apply_redirects.ts +++ b/packages/router/src/apply_redirects.ts @@ -142,7 +142,8 @@ export class ApplyRedirects { ): UrlSegmentGroup { const updatedSegments = this.createSegments(redirectTo, group.segments, segments, posParams); - let children: {[n: string]: UrlSegmentGroup} = {}; + // Keyed by outlet name, which can be `__proto__`, so use a null-prototype map. + let children: {[n: string]: UrlSegmentGroup} = Object.create(null); Object.entries(group.children).forEach(([name, child]) => { children[name] = this.createSegmentGroup(redirectTo, child, segments, posParams); }); diff --git a/packages/router/src/create_url_tree.ts b/packages/router/src/create_url_tree.ts index f2412c7a1341..a7612625abad 100644 --- a/packages/router/src/create_url_tree.ts +++ b/packages/router/src/create_url_tree.ts @@ -225,7 +225,8 @@ function replaceSegment( oldSegment: UrlSegmentGroup, newSegment: UrlSegmentGroup, ): UrlSegmentGroup { - const children: {[key: string]: UrlSegmentGroup} = {}; + // Keyed by outlet name, which can be `__proto__`, so use a null-prototype map. + const children: {[key: string]: UrlSegmentGroup} = Object.create(null); Object.entries(current.children).forEach(([outletName, c]) => { if (c === oldSegment) { children[outletName] = newSegment; @@ -419,7 +420,8 @@ function updateSegmentGroupChildren( return new UrlSegmentGroup(segmentGroup.segments, {}); } else { const outlets = getOutlets(commands); - const children: {[key: string]: UrlSegmentGroup} = {}; + // Keyed by outlet name, which can be `__proto__`, so use a null-prototype map. + const children: {[key: string]: UrlSegmentGroup} = Object.create(null); // If the set of commands applies to anything other than the primary outlet and the child // segment is an empty path primary segment on its own, we want to apply the commands to the // empty child path rather than here. The outcome is that the empty primary child is effectively diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index ad72276ef8e0..0c7b9045e79b 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -761,7 +761,11 @@ class UrlParser { // parse `(a/b//outlet_name:c/d)` private parseParens(allowPrimary: boolean, depth: number): {[outlet: string]: UrlSegmentGroup} { - const segments: {[key: string]: UrlSegmentGroup} = {}; + // The outlet name is taken verbatim from the URL, so it can be `__proto__`. Indexing a plain + // object with that key assigns through the inherited `__proto__` setter instead of creating an + // outlet, which drops the outlet and mutates the map's prototype (and throws under Node's + // `--disable-proto=throw`). A null-prototype map makes `__proto__` an ordinary key. + const segments: {[key: string]: UrlSegmentGroup} = Object.create(null); this.capture('('); while (!this.consumeOptional(')') && this.remaining.length > 0) { @@ -838,7 +842,8 @@ export function createRoot(rootCandidate: UrlSegmentGroup): UrlSegmentGroup { * root but the `a` route lives under an empty path primary route. */ export function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup { - const newChildren: Record = {}; + // Keyed by outlet name, which can be `__proto__`, so use a null-prototype map (see `parseParens`). + const newChildren: Record = Object.create(null); for (const [childOutlet, child] of Object.entries(segmentGroup.children)) { const childCandidate = squashSegmentGroup(child); // moves named children in an empty path primary child into this group diff --git a/packages/router/test/url_serializer.spec.ts b/packages/router/test/url_serializer.spec.ts index 9258edfae2df..70329510ab0b 100644 --- a/packages/router/test/url_serializer.spec.ts +++ b/packages/router/test/url_serializer.spec.ts @@ -132,6 +132,17 @@ describe('url serializer', () => { expect(url.serialize(tree)).toEqual('/one(left:two/three)'); }); + it('should parse a secondary segment named "__proto__"', () => { + const tree = url.parse('/one(__proto__:two)'); + + expectSegment(tree.root.children[PRIMARY_OUTLET], 'one'); + expectSegment(tree.root.children['__proto__'], 'two'); + expect(tree.root.numberOfChildren).toEqual(2); + expect(Object.getPrototypeOf(tree.root.children)).toBeNull(); + + expect(url.serialize(tree)).toEqual('/one(__proto__:two)'); + }); + it('should parse an empty secondary segment group', () => { const tree = url.parse('/one()');