fix: return real constructor on drafts (fixes lodash.isEmpty crash), keep __proto__ pollution guard#1269
fix: return real constructor on drafts (fixes lodash.isEmpty crash), keep __proto__ pollution guard#1269jonatankruszewski wants to merge 1 commit into
Conversation
…llution guard The 11.1.9 hardening (48fc378) wrapped both constructor and __proto__ reads in a sanitizing Proxy. The __proto__ half cleanly blocks draft.__proto__.x = 1 from reaching Object.prototype and is kept. The constructor half cannot work: constructor resolves to a function whose prototype is a non-configurable, non-writable data property, so a wrapping Proxy that hides it violates a Proxy invariant and throws (TypeError: 'get' on proxy: property 'prototype' ...), crashing ecosystem inspection such as lodash.isEmpty(draft). Mutating draft.constructor.prototype is plain-JS Object.prototype mutation immer is not a vector for and cannot intercept without breaking draft.constructor === Object. Keep the __proto__ wrapper; drop the constructor wrapper and the has/set reserved-key blocks. Replace the added tests with ones that distinguish a clean block from a crash. All 3697 tests pass.
|
This also resolves #1266 (unstable #1267 caches the wrapper so wrap(Object) === wrap(Object) // true (identity fixed by caching)
wrap(Object).prototype // TypeError: 'get' on proxy: property 'prototype' ...Returning the real |
|
This also fixes #1265. It's the same regression reached through a different lodash path: there Verified against that issue's repro on this branch: const {produce} = require("immer")
const _ = require("lodash") // 4.18.1
produce(
[
{title: "Learn Macroeconomics", done: false},
{title: "Try Immer", done: false}
],
draft => {
draft.push({title: "Tweet about it"})
draft[1].done = true
_.isEqual(draft, [
{title: "Learn Macroeconomics", done: false},
{title: "Try Immer", done: true},
{title: "Tweet about it"}
]) // no longer throws
}
) |
Fixes #1268
Problem
The prototype-pollution hardening in #1259 (
48fc378) wrapsconstructorreads on a draft in aProxy. Becausedraft.constructoris the realObject, whoseprototypeis a non-configurable, non-writable data property, readingdraft.constructor.prototypeviolates a Proxy invariant and throws:This crashes any ecosystem code that inspects
value.constructor.prototypeon a draft — most notablylodash.isEmpty()inside a Redux Toolkit reducer.Why the
constructorwrapper can't workOn a draft,
draft.constructoris the realObjectanddraft.constructor.prototypeis the realObject.prototype, sodraft.constructor.prototype.x = 1is literallyObject.prototype.x = 1— plain JavaScript reachable in any reducer with or without a draft. Immer is not the vector, and cannot intercept it without returning a fakeconstructor(which breaksconstructor === Object). The wrapper's only effect on that vector was to crash. No version ever actually blocked it — 11.1.8 pollutes, 11.1.9+ throws, plain JS pollutes.Change
__proto__protection — but as a small dedicated wrapper (protoGuardTraps).__proto__resolves to a plain object, so it can be virtualized without hitting the invariant, and it still cleanly blocksdraft.__proto__.x = 1.constructorwrapper — return the real constructor. Fixes the crash, restoresconstructor === Object, and stopsnew Proxy(...)from throwing on drafts that carry a non-objectconstructordata key.has/setreserved-key blocks — restore'constructor' in draftand stop silently dropping writes to own data keys namedconstructor/prototype.setPrototypeOfand the patch-layer guards are untouched, so this is strictly safer than 11.1.8 (it adds the__proto__block) with the crash removed.Tests
The three tests added in #1259 wrapped each attack in
try/catchand asserted only thatObject.prototypestayed clean, so a crash and a real block passed identically (which is how the regression reached a release). Replaced them with tests that distinguish the two:constructor/constructor.prototypereads don't throw and=== Object;Object.setPrototypeOf(draft, …)throws;draft.__proto__.x = 1does not pollute (asserts blocked, not threw);__proto__guard wrapper.All existing tests pass.