Skip to content

security(helpers): filter __proto__ and constructor in deepCopy#26

Merged
Goosterhof merged 2 commits into
mainfrom
security/deepcopy-prototype-pollution
Apr 13, 2026
Merged

security(helpers): filter __proto__ and constructor in deepCopy#26
Goosterhof merged 2 commits into
mainfrom
security/deepcopy-prototype-pollution

Conversation

@Goosterhof
Copy link
Copy Markdown
Contributor

Summary

Closes Sapper M1 finding M5 — deepCopy prototype pollution (field report lives in the war-room meta-repo, not this one).

The bug

JSON.parse treats __proto__ and constructor as literal own enumerable keys, unlike object literals where __proto__ is a getter/setter on Object.prototype. That means:

```js
const payload = JSON.parse('{"proto": {"polluted": "yes"}}');
Object.keys(payload); // ["proto"] ← own enumerable
```

The previous deepCopy iterated with Object.keys and assigned via copiedObject[key] = .... When key === "__proto__", that assignment triggered the prototype setter, replacing the copy's prototype with an attacker-controlled object. A consumer checking inherited properties (e.g., if (obj.admin)) without Object.hasOwn would then be fooled.

The fix

One line added to the iteration:

```ts
for (const key of Object.keys(toCopy)) {
if (key === "proto" || key === "constructor") continue;
copiedObject[key] = deepCopy(...);
}
```

No legitimate caller supplies plain-object data with a literal `proto` own property — that only arises from untrusted input. `constructor` filtering is defense-in-depth (it never triggers prototype ops, but can shadow the real `constructor` on the copy, which is a known attack surface in some frameworks).

Tests

3 new cases in `prototype pollution resistance`:

  1. Preserves prototype on `proto` injection — `Object.getPrototypeOf(copy)` remains `Object.prototype`, inherited `polluted` is undefined.
  2. Drops literal `constructor` key — `Object.hasOwn(copy, "constructor")` is `false`; global `Object.prototype` is not polluted.
  3. Preserves safe keys when dangerous keys are present — `safe` and `other` still copy correctly; `polluted` still inaccessible.

All 8 local gates pass: format, lint, build, typecheck, lint:pkg, 100% coverage, 100% mutation score on `deep-copy.ts` (23 mutants killed).

Release

Bumps `@script-development/fs-helpers` to `0.1.1`. On merge the publish workflow fires (triggered by `**/package.json` change on `main`), ships the patch to npm.

Test plan

  • CI `check` job passes
  • Ally review
  • Merge triggers publish of 0.1.1 with sigstore provenance
  • Spot-check npm: @script-development/fs-helpers@0.1.1 present

🤖 Generated with Claude Code

Addresses Sapper M1 finding M5 (prototype pollution via JSON.parse input).

JSON.parse treats __proto__ and constructor as literal own enumerable
properties, unlike object literals. Passing such parsed data to deepCopy
would assign copiedObject.__proto__ = attackerValue, which triggers the
prototype setter and replaces the copy's prototype with an attacker-
controlled object. A consumer checking inherited properties (e.g.,
`if (obj.admin)`) without Object.hasOwn could then be fooled.

Fix: skip __proto__ and constructor keys during the key iteration. No
legitimate deepCopy caller supplies plain-object data with a literal
"__proto__" own property — that only arises from untrusted input.

Tests: 3 new cases verify the prototype is preserved, constructor is
dropped, and other safe keys in the same payload are still copied.
Coverage + mutation score remain 100% on deep-copy.ts.

Bumps fs-helpers to 0.1.1 (triggers publish workflow on merge).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 13, 2026

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4214a54
Status: ✅  Deploy successful!
Preview URL: https://911647b2.fs-packages.pages.dev
Branch Preview URL: https://security-deepcopy-prototype.fs-packages.pages.dev

View logs

@Goosterhof Goosterhof merged commit 4202cf4 into main Apr 13, 2026
2 checks passed
@Goosterhof Goosterhof deleted the security/deepcopy-prototype-pollution branch April 13, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants