Skip to content

structuredClone: own __proto__ property is mis-cloned via assignment (triggers setter) #420

Description

@mnaza

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
  }
}
  1. out[k] = … with k === "__proto__" invokes the Object.prototype __proto__ setter instead of defining an own property.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions