Skip to content

Commit 6a86cd3

Browse files
committed
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.
1 parent 7d913a6 commit 6a86cd3

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

packages/solid/store/src/store.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ export function setProperty(
215215
value: any,
216216
deleting: boolean = false
217217
): void {
218+
// Prototype pollution guard: refuse to redefine the prototype chain via
219+
// `state.__proto__ = ...` or to overwrite built-in prototype links.
220+
if (property === "__proto__") {
221+
if (IS_DEV)
222+
console.warn(`Refusing to set "__proto__" on a store (prototype pollution guard).`);
223+
return;
224+
}
218225
if (!deleting && state[property] === value) return;
219226
const prev = state[property],
220227
len = state.length;
@@ -274,6 +281,21 @@ export function updatePath(current: StoreNode, path: any[], traversed: PropertyK
274281
const partType = typeof part,
275282
isArray = Array.isArray(current);
276283

284+
// Prototype pollution guard: refuse to traverse into dangerous keys
285+
// (e.g. `setStore("__proto__", ...)` or
286+
// `setStore("constructor", "prototype", ...)`), which would otherwise
287+
// let callers reach and mutate Object.prototype / Function.prototype.
288+
if (
289+
partType === "string" &&
290+
(part === "__proto__" || part === "constructor" || part === "prototype")
291+
) {
292+
if (IS_DEV)
293+
console.warn(
294+
`Refusing to traverse into "${part}" on a store (prototype pollution guard).`
295+
);
296+
return;
297+
}
298+
277299
if (Array.isArray(part)) {
278300
// Ex. update('data', [2, 23], 'label', l => l + ' !!!');
279301
for (let i = 0; i < part.length; i++) {

packages/solid/store/test/store.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,33 @@ describe("In Operator", () => {
799799
});
800800
});
801801

802+
describe("Prototype pollution guard", () => {
803+
test("setStore cannot pollute Object.prototype via __proto__ path", () => {
804+
const [, setStore] = createStore<Record<string, any>>({ a: 1 });
805+
setStore("__proto__", "polluted_a", true);
806+
expect(({} as any).polluted_a).toBeUndefined();
807+
});
808+
809+
test("setStore cannot pollute Object.prototype via __proto__ merge", () => {
810+
const [, setStore] = createStore<Record<string, any>>({ a: 1 });
811+
setStore("__proto__", { polluted_b: true });
812+
expect(({} as any).polluted_b).toBeUndefined();
813+
});
814+
815+
test("setStore cannot pollute via constructor.prototype", () => {
816+
const [, setStore] = createStore<Record<string, any>>({ a: 1 });
817+
setStore("constructor", "prototype", "polluted_c", true);
818+
expect(({} as any).polluted_c).toBeUndefined();
819+
});
820+
821+
test("setStore cannot pollute via JSON-parsed __proto__ own property merge", () => {
822+
const [, setStore] = createStore<Record<string, any>>({ a: 1 });
823+
const evil = JSON.parse('{"__proto__": {"polluted_d": true}}');
824+
setStore(evil);
825+
expect(({} as any).polluted_d).toBeUndefined();
826+
});
827+
});
828+
802829
// type tests
803830

804831
// NotWrappable keys are ignored

0 commit comments

Comments
 (0)