Prototype Pollution
Description
This commit alters the segment explorer trie to use null-prototype objects for its children maps. The previous implementation used plain {} for child nodes, which can collide with Object.prototype properties (e.g., 'constructor', 'toString') when segment names are user-controlled. This change prevents those collisions by ensuring the internal maps do not inherit from Object.prototype, reducing the risk of prototype pollution or logic errors in the segment explorer when handling user-supplied segment names.
Commit Details
Author: Joseph
Date: 2026-07-02 12:35 UTC
Message:
fix: handle prototype-colliding segment names in segment explorer trie (#95403)
Fixes: https://github.com/vercel/next.js/issues/95395
```console
let x = {}
x['constructor']; // function
x['toString']; // function
x = Object.create(null);
x['constructor']; // undefined
x['toString'] // undefined
```
Triage Assessment
Vulnerability Type: Prototype Pollution
Confidence: MEDIUM
Reasoning:
The change prevents prototype pollution by using null-prototype objects for trie children, avoiding collisions with Object.prototype properties like 'constructor' or 'toString' when segment names are user-controlled. This mitigates a class of security issues related to prototype pollution in the segment explorer trie.
Verification Assessment
Vulnerability Type: Prototype Pollution
Confidence: MEDIUM
Affected Versions: All versions prior to 16.2.2 (before this commit)
Code Diff
diff --git a/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.test.tsx b/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.test.tsx
index a4812bb49fe..8b05181b2f0 100644
--- a/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.test.tsx
+++ b/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.test.tsx
@@ -105,6 +105,85 @@ describe('Segment Explorer', () => {
})
})
+ test('handle segments that collide with Object.prototype members', () => {
+ insertSegmentNode(
+ createSegmentNode({ pagePath: '/constructor/page.js', type: 'page' })
+ )
+ insertSegmentNode(
+ createSegmentNode({ pagePath: '/toString/page.js', type: 'page' })
+ )
+
+ const { result } = renderHook(useSegmentTree)
+
+ expect(result.current).toEqual({
+ children: {
+ '': {
+ children: {
+ constructor: {
+ children: {
+ 'page.js': {
+ children: {},
+ value: {
+ pagePath: '/constructor/page.js',
+ type: 'page',
+ boundaryType: null,
+ setBoundaryType: expect.anything(),
+ },
+ },
+ },
+ value: undefined,
+ },
+ toString: {
+ children: {
+ 'page.js': {
+ children: {},
+ value: {
+ pagePath: '/toString/page.js',
+ type: 'page',
+ boundaryType: null,
+ setBoundaryType: expect.anything(),
+ },
+ },
+ },
+ value: undefined,
+ },
+ },
+ value: undefined,
+ },
+ },
+ value: undefined,
+ })
+
+ removeSegmentNode(
+ createSegmentNode({ pagePath: '/constructor/page.js', type: 'page' })
+ )
+
+ expect(result.current).toEqual({
+ children: {
+ '': {
+ children: {
+ toString: {
+ children: {
+ 'page.js': {
+ children: {},
+ value: {
+ pagePath: '/toString/page.js',
+ type: 'page',
+ boundaryType: null,
+ setBoundaryType: expect.anything(),
+ },
+ },
+ },
+ value: undefined,
+ },
+ },
+ value: undefined,
+ },
+ },
+ value: undefined,
+ })
+ })
+
test('remove node in the middle', () => {
insertSegmentNode(
createSegmentNode({ pagePath: '/a/b/@sidebar/page.js', type: 'page' })
diff --git a/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.ts b/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.ts
index 8145fe3e827..99938333027 100644
--- a/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.ts
+++ b/packages/next/src/next-devtools/dev-overlay/segment-explorer-trie.ts
@@ -62,9 +62,11 @@ function createTrie<Value = string>({
getCharacters?: (item: Value) => string[]
compare?: (a: Value | undefined, b: Value | undefined) => boolean
}): Trie<Value> {
+ // Null-prototype children so segment names that collide with
+ // Object.prototype members (e.g. "constructor", "toString") work correctly.
let root: TrieNode<Value> = {
value: undefined,
- children: {},
+ children: Object.create(null),
}
function markUpdated() {
@@ -82,7 +84,7 @@ function createTrie<Value = string>({
currentNode.children[segment] = {
value: undefined,
// Skip value for intermediate nodes
- children: {},
+ children: Object.create(null),
}
}
currentNode = currentNode.children[segment]