Skip to content

fix(store): harden setState against prototype pollution#1

Merged
afiiif merged 1 commit intoafiiif:mainfrom
RakulAgn:fix/prototype-pollution-setstate
Apr 13, 2026
Merged

fix(store): harden setState against prototype pollution#1
afiiif merged 1 commit intoafiiif:mainfrom
RakulAgn:fix/prototype-pollution-setstate

Conversation

@RakulAgn
Copy link
Copy Markdown
Contributor

The change-detection loop in setState used for...in over the incoming partial, which walks the full prototype chain. Any enumerable property inherited from Object.prototype (whether introduced by a polluted global, a faulty polyfill, or an untrusted payload parsed into the call site) would be treated as a legitimate state key, pushed into changedKeys, and forwarded to subscribers. Downstream consumers tracking those keys via the proxy-based useStore hook would then observe phantom updates for fields the caller never actually set.

Replace the iteration with Object.keys(newValue) so only own enumerable string keys participate in the shallow diff, and explicitly skip __proto__, constructor, and prototype as defense-in-depth against payloads (e.g. JSON.parse output) where these names appear as own properties. The subsequent { ...prevState, ...newValue } merge is already safe because object spread uses CreateDataProperty semantics, but filtering upstream keeps the dangerous keys from ever reaching changedKeys or the subscriber notification path.

Behaviour is unchanged for well-formed state objects: Object.keys enumerates the same own keys for...in did, with no measurable perf impact. Full test suite (90 tests) passes.

The change-detection loop in `setState` used `for...in` over the
incoming partial, which walks the full prototype chain. Any enumerable
property inherited from `Object.prototype` (whether introduced by a
polluted global, a faulty polyfill, or an untrusted payload parsed into
the call site) would be treated as a legitimate state key, pushed into
`changedKeys`, and forwarded to subscribers. Downstream consumers
tracking those keys via the proxy-based `useStore` hook would then
observe phantom updates for fields the caller never actually set.

Replace the iteration with `Object.keys(newValue)` so only own
enumerable string keys participate in the shallow diff, and explicitly
skip `__proto__`, `constructor`, and `prototype` as defense-in-depth
against payloads (e.g. `JSON.parse` output) where these names appear as
own properties. The subsequent `{ ...prevState, ...newValue }` merge is
already safe because object spread uses `CreateDataProperty` semantics,
but filtering upstream keeps the dangerous keys from ever reaching
`changedKeys` or the subscriber notification path.

Behaviour is unchanged for well-formed state objects: `Object.keys`
enumerates the same own keys `for...in` did, with no measurable perf
impact. Full test suite (90 tests) passes.
Copy link
Copy Markdown
Contributor Author

@RakulAgn RakulAgn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afiiif
Copy link
Copy Markdown
Owner

afiiif commented Apr 13, 2026

Nice! 🙌
Merging it...

Thanks @RakulAgn

@afiiif afiiif merged commit b3527cd into afiiif:main Apr 13, 2026
1 check passed
@RakulAgn
Copy link
Copy Markdown
Contributor Author

Nice! 🙌 Merging it...

Thanks @RakulAgn

Have you Checked the Email and Concept

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