From 6a86cd3c7d9271ea65f40c20bb3a2b03edde69de Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 23 Apr 2026 09:20:41 +0200 Subject: [PATCH] fix(store): prevent prototype pollution via setStore paths Reject writes to `__proto__` in `setProperty` and refuse to traverse through `__proto__`, `constructor`, and `prototype` in `updatePath`. This closes a prototype-pollution vector where attacker-controlled path segments (e.g. from query params, form data, or a JSON payload merged via `setStore(obj)`) could reach and mutate `Object.prototype` or `Function.prototype` globally. Covers all mutation entry points that funnel through `setProperty`: `createStore` / `setStore`, `createMutable` (proxy set trap), `produce` (setterTraps), `reconcile`, and `mergeStoreNode`. Adds regression tests for each reachable pollution path. --- packages/solid/store/src/store.ts | 22 ++++++++++++++++++++ packages/solid/store/test/store.spec.ts | 27 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/solid/store/src/store.ts b/packages/solid/store/src/store.ts index 3276b3a6d..236d07ee9 100644 --- a/packages/solid/store/src/store.ts +++ b/packages/solid/store/src/store.ts @@ -215,6 +215,13 @@ export function setProperty( value: any, deleting: boolean = false ): void { + // Prototype pollution guard: refuse to redefine the prototype chain via + // `state.__proto__ = ...` or to overwrite built-in prototype links. + if (property === "__proto__") { + if (IS_DEV) + console.warn(`Refusing to set "__proto__" on a store (prototype pollution guard).`); + return; + } if (!deleting && state[property] === value) return; const prev = state[property], len = state.length; @@ -274,6 +281,21 @@ export function updatePath(current: StoreNode, path: any[], traversed: PropertyK const partType = typeof part, isArray = Array.isArray(current); + // Prototype pollution guard: refuse to traverse into dangerous keys + // (e.g. `setStore("__proto__", ...)` or + // `setStore("constructor", "prototype", ...)`), which would otherwise + // let callers reach and mutate Object.prototype / Function.prototype. + if ( + partType === "string" && + (part === "__proto__" || part === "constructor" || part === "prototype") + ) { + if (IS_DEV) + console.warn( + `Refusing to traverse into "${part}" on a store (prototype pollution guard).` + ); + return; + } + if (Array.isArray(part)) { // Ex. update('data', [2, 23], 'label', l => l + ' !!!'); for (let i = 0; i < part.length; i++) { diff --git a/packages/solid/store/test/store.spec.ts b/packages/solid/store/test/store.spec.ts index 6fc6e28c1..1d8736448 100644 --- a/packages/solid/store/test/store.spec.ts +++ b/packages/solid/store/test/store.spec.ts @@ -799,6 +799,33 @@ describe("In Operator", () => { }); }); +describe("Prototype pollution guard", () => { + test("setStore cannot pollute Object.prototype via __proto__ path", () => { + const [, setStore] = createStore>({ a: 1 }); + setStore("__proto__", "polluted_a", true); + expect(({} as any).polluted_a).toBeUndefined(); + }); + + test("setStore cannot pollute Object.prototype via __proto__ merge", () => { + const [, setStore] = createStore>({ a: 1 }); + setStore("__proto__", { polluted_b: true }); + expect(({} as any).polluted_b).toBeUndefined(); + }); + + test("setStore cannot pollute via constructor.prototype", () => { + const [, setStore] = createStore>({ a: 1 }); + setStore("constructor", "prototype", "polluted_c", true); + expect(({} as any).polluted_c).toBeUndefined(); + }); + + test("setStore cannot pollute via JSON-parsed __proto__ own property merge", () => { + const [, setStore] = createStore>({ a: 1 }); + const evil = JSON.parse('{"__proto__": {"polluted_d": true}}'); + setStore(evil); + expect(({} as any).polluted_d).toBeUndefined(); + }); +}); + // type tests // NotWrappable keys are ignored