Skip to content

Commit 848b933

Browse files
committed
fix: Revert "prevent prototype pollution via constructor.prototype access (CVE-2026-XXXX) (#1259)"
This reverts commit 48fc378. This fixes #1266 and #1268, #1265 while the security reported could not find a reasonable way to show the designated fixes was closing a real vulnerability, nor could I think of anything.
1 parent 60ca295 commit 848b933

3 files changed

Lines changed: 1 addition & 140 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,5 @@
55
"javascript.validate.enable": false,
66
"typescript.tsdk": "node_modules/typescript/lib",
77
"jest.enableInlineErrorMessages": true,
8-
"cSpell.enabled": true,
9-
"chat.tools.terminal.autoApprove": {
10-
"yarn vitest": true
11-
}
8+
"cSpell.enabled": true
129
}

__tests__/base.js

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2953,98 +2953,6 @@ function runBaseTest(
29532953
expect(result.objConstructed).toEqual(new Object().constructor(1))
29542954
})
29552955

2956-
it("does not allow prototype pollution via reserved constructor access", () => {
2957-
const pollutedKey = "__immer_test_polluted__"
2958-
const original = Object.prototype[pollutedKey]
2959-
2960-
delete Object.prototype[pollutedKey]
2961-
2962-
try {
2963-
// The attack should either throw an error or silently fail
2964-
// but NOT pollute Object.prototype
2965-
let hadError = false
2966-
try {
2967-
produce({}, draft => {
2968-
draft.constructor.prototype[pollutedKey] = true
2969-
draft["__proto__"][pollutedKey] = true
2970-
})
2971-
} catch (e) {
2972-
// Expected: error when trying to set on undefined/frozen objects
2973-
hadError = true
2974-
}
2975-
2976-
// The critical check: Object.prototype must not be polluted
2977-
// either because we threw an error OR because the assignment was silently blocked
2978-
expect(Object.prototype[pollutedKey]).toBeUndefined()
2979-
} finally {
2980-
if (original === undefined) {
2981-
delete Object.prototype[pollutedKey]
2982-
} else {
2983-
Object.prototype[pollutedKey] = original
2984-
}
2985-
}
2986-
})
2987-
2988-
it("blocks prototype pollution via stored constructor reference (CVE bypass)", () => {
2989-
const pollutedKey = "__immer_test_ref__"
2990-
const original = Object.prototype[pollutedKey]
2991-
2992-
delete Object.prototype[pollutedKey]
2993-
2994-
try {
2995-
// Attack 3 from CVE: store constructor reference and mutate
2996-
let hadError = false
2997-
try {
2998-
produce({data: {}}, draft => {
2999-
const ctor = draft.data.constructor
3000-
ctor.prototype[pollutedKey] = true
3001-
})
3002-
} catch (e) {
3003-
hadError = true
3004-
}
3005-
3006-
expect(Object.prototype[pollutedKey]).toBeUndefined()
3007-
} finally {
3008-
if (original === undefined) {
3009-
delete Object.prototype[pollutedKey]
3010-
} else {
3011-
Object.prototype[pollutedKey] = original
3012-
}
3013-
}
3014-
})
3015-
3016-
it("blocks prototype pollution via Object.assign with malicious payload", () => {
3017-
const pollutedKey = "__immer_test_assign__"
3018-
const original = Object.prototype[pollutedKey]
3019-
3020-
delete Object.prototype[pollutedKey]
3021-
3022-
try {
3023-
// Simulates the real-world attack scenario where user input is Object.assign'd to draft
3024-
const userInput = {
3025-
constructor: {prototype: {[pollutedKey]: true}}
3026-
}
3027-
3028-
let hadError = false
3029-
try {
3030-
produce({}, draft => {
3031-
Object.assign(draft, userInput)
3032-
})
3033-
} catch (e) {
3034-
hadError = true
3035-
}
3036-
3037-
// Must NOT pollute via Object.assign path
3038-
expect(Object.prototype[pollutedKey]).toBeUndefined()
3039-
} finally {
3040-
if (original === undefined) {
3041-
delete Object.prototype[pollutedKey]
3042-
} else {
3043-
Object.prototype[pollutedKey] = original
3044-
}
3045-
}
3046-
})
3047-
30482956
it("should handle equality correctly - 1", () => {
30492957
const baseState = {
30502958
y: 3 / 0,

src/core/proxy.ts

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -110,32 +110,6 @@ export const objectTraps: ProxyHandler<ProxyState> = {
110110
get(state, prop) {
111111
if (prop === DRAFT_STATE) return state
112112

113-
// Guard against prototype pollution via constructor and __proto__
114-
// We allow access but wrap in a proxy that blocks prototype chain traversal
115-
if (prop === "constructor" || prop === "__proto__") {
116-
const source = latest(state)
117-
const value = source[prop]
118-
// Return a proxy that allows calling the constructor but blocks access to prototype
119-
return new Proxy(value || {}, {
120-
get: (target, key) => {
121-
// Block __proto__ and prototype access chains
122-
if (key === "__proto__" || key === "prototype") {
123-
return Object.freeze(Object.create(null))
124-
}
125-
// Allow normal property access for legitimate use
126-
return Reflect.get(target, key)
127-
},
128-
set: () => {
129-
// Silently ignore writes to prevent pollution
130-
return true
131-
},
132-
apply: (target, thisArg, args) => {
133-
// Allow constructor to be called as a function (e.g., draft.arr.constructor(1))
134-
return Reflect.apply(target as Function, thisArg, args)
135-
}
136-
})
137-
}
138-
139113
let arrayPlugin = state.scope_.arrayMethodsPlugin_
140114
const isArrayWithStringProp =
141115
state.type_ === ArchType.Array && typeof prop === "string"
@@ -183,14 +157,6 @@ export const objectTraps: ProxyHandler<ProxyState> = {
183157
return value
184158
},
185159
has(state, prop) {
186-
// Block reserved properties from being detected
187-
if (
188-
prop === "constructor" ||
189-
prop === "__proto__" ||
190-
prop === "prototype"
191-
) {
192-
return false
193-
}
194160
return prop in latest(state)
195161
},
196162
ownKeys(state) {
@@ -201,16 +167,6 @@ export const objectTraps: ProxyHandler<ProxyState> = {
201167
prop: string /* strictly not, but helps TS */,
202168
value
203169
) {
204-
// Guard against prototype pollution - prevent assignment to reserved properties
205-
// that could lead to Object.prototype pollution
206-
if (
207-
prop === "constructor" ||
208-
prop === "__proto__" ||
209-
prop === "prototype"
210-
) {
211-
return true
212-
}
213-
214170
const desc = getDescriptorFromProto(latest(state), prop)
215171
if (desc?.set) {
216172
// special case: if this write is captured by a setter, we have

0 commit comments

Comments
 (0)