fix: keep decoded __proto__ keys as own properties - #133
Conversation
Both loader-data decoders wrote object keys with `result[key] = value`.
In browsers, assigning to `__proto__` sets the object's prototype
instead of creating an own property, so a loader returning
`{"__proto__": {"isAdmin": true}}` reached the client with the key
gone and `isAdmin` inherited. Keys are now written with
Object.defineProperty on the page-load, data-request, streamed, and
deferred decode paths.
The regression test decodes in a child `deno eval` that installs the
browser `__proto__` accessor, since Deno assigns it as an ordinary
property and the test process must not mutate globals.
Closes #132
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Security review at No findings. The fix closes juniper#132 on every decode path that builds an object from payload keys. Both new call sites are load-bearing: reverting either one turns its own path red, and no other path. The result keeps the default prototype, and normal data comes out the same as before. Severity of what this closes (for the record)
Decode paths examined (at
|
Summary
Both loader-data decoders wrote object keys with
result[key] = value. In browsers, assigning to__proto__sets the object's prototype instead of creating an own property. So a loader that returns untrusted JSON such as{"__proto__": {"isAdmin": true}}reached the client with the key missing fromObject.keys/JSON.stringify, andprefs.isAdmin === truethrough inheritance. The server andObject.prototypeare unaffected. This bug already existed in 0.11.5.Changes
src/_serialization.ts: new privatedefineOwnValuehelper that writes a key withObject.defineProperty(enumerable, writable, configurable).restoreValueuses it; that covers page loads, data requests, and deferred values that resolved.restoreValueWithPendingPromisesuses it too; that covers the initial chunk of a streamed response.processValue,processValueForStreaming) are unchanged. They copy keys from server-side source objects, and Deno assigns__proto__as an ordinary property.Object.fromEntries, so the raw decode already kept the own key. The key was lost only in juniper's own assignments.Testing
src/_serialization.test.ts: "decoding an own proto key where assignment sets the prototype". It runs the decode in a childdeno eval, so the test process never mutates globals.Object.prototype.__proto__accessor. Before decoding, it checks that assignment now really sets the prototype.Object.prototype, andisAdminis not inherited.ownKeys: ["name"],hasOwnProto: false,protoIsObjectPrototype: false,isAdmin: true. After the fix it passes.deno task test: 34 passed (387 steps), 0 failed.deno task checkpasses, including doc-lint.Related
Found by the security review of #131, which does not introduce or change this bug. A trial merge with #131 is clean.
Closes #132
🤖 Generated with Claude Code