Summary
structuredClone() mis-clones an object that has an own enumerable __proto__ data property — exactly what JSON.parse('{"__proto__":{…}}') produces, and JSON round-tripping is the stated motivation of #390. Instead of creating an own __proto__ data property on the clone, the assignment triggers the inherited __proto__ setter and reparents the clone. Separately, plain objects come back with the source's prototype rather than Object.prototype.
Root cause
Generic branch of _structuredClone (crates/obscura-js/js/bootstrap.js:5281-5287):
const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
seen.set(value, out);
for (const k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
out[k] = _structuredClone(value[k], seen); // assignment, not define
}
}
out[k] = … with k === "__proto__" invokes the Object.prototype __proto__ setter instead of defining an own property.
Object.create(Object.getPrototypeOf(value)) copies the source prototype; Chrome's structured clone returns a plain Object.prototype-based object for ordinary objects.
Reproduction
const src = JSON.parse('{"__proto__":{"polluted":true},"a":1}');
const clone = structuredClone(src);
Object.prototype.hasOwnProperty.call(clone, "__proto__"); // Chrome: true | obscura: false
Object.getPrototypeOf(clone) === Object.prototype; // Chrome: true | obscura: false
clone.polluted; // Chrome: undefined | obscura: true
Attacker-controlled prototype smuggling into the clone (not global Object.prototype pollution, but a real correctness + security divergence).
Expected (Chrome parity)
Plain objects clone onto Object.prototype; an own __proto__ data property is reproduced as an own data property (via a define, not assignment), leaving the clone's prototype untouched.
Fix direction
Build plain objects on a clean base and copy properties with Object.defineProperty / a create-data-property helper (or set them via Reflect.defineProperty) so __proto__ and accessor-shadowed keys become own data properties. Preserve current Array handling.
Summary
structuredClone()mis-clones an object that has an own enumerable__proto__data property — exactly whatJSON.parse('{"__proto__":{…}}')produces, and JSON round-tripping is the stated motivation of #390. Instead of creating an own__proto__data property on the clone, the assignment triggers the inherited__proto__setter and reparents the clone. Separately, plain objects come back with the source's prototype rather thanObject.prototype.Root cause
Generic branch of
_structuredClone(crates/obscura-js/js/bootstrap.js:5281-5287):out[k] = …withk === "__proto__"invokes theObject.prototype__proto__setter instead of defining an own property.Object.create(Object.getPrototypeOf(value))copies the source prototype; Chrome's structured clone returns a plainObject.prototype-based object for ordinary objects.Reproduction
Attacker-controlled prototype smuggling into the clone (not global
Object.prototypepollution, but a real correctness + security divergence).Expected (Chrome parity)
Plain objects clone onto
Object.prototype; an own__proto__data property is reproduced as an own data property (via a define, not assignment), leaving the clone's prototype untouched.Fix direction
Build plain objects on a clean base and copy properties with
Object.defineProperty/ a create-data-property helper (or set them viaReflect.defineProperty) so__proto__and accessor-shadowed keys become own data properties. Preserve current Array handling.