GHSA-9m6g-wc8r-q59c scimPatch vulnerable to prototype pollution via unfiltered keys in patch
Summary`scim-patch` performs prototype pollution when applying a SCIM PATCH operation whose `value` object contains a key like `"__proto__.someProp"`. After one such patch,
`Object.prototype.someProp` is set process-wide, affecting every plain object in the Node process.Any service that calls `scimPatch()` on attacker-controlled JSON (i.e. any SCIM endpoint accepting `PATCH` from an external IdP) is exploitable on a stock Node runtime.Impact**Class:** Prototype pollution ([CWE-1321](https://cwe.mitre.org/data/definitions/1321.html))**Affected versions:** `<= 0.9.0` (current HEAD `871b1e2`)**Attack vector:** Network — sent as part of a normal SCIM `PATCH /Users/:id` request body.**Privileges required:** Whatever the SCIM endpoint requires. For most integrations that's a provisioned IdP, which is "low" in CVSS terms (any authenticated provisioning client).**Scope:** Changed — the bug is in a SCIM library but the side effect (`Object.prototype` mutation) leaks into the entire Node process.Downstream consequences depend on what other code reads from plain objects. Realistic outcomes observed in similar bugs:**Privilege escalation** if any auth/middleware code checks `actor.isAdmin` / `req.user.admin` / similar boolean flags against a plain object that *expects* the key to be absent.**Logic bypass / DoS** if any code branches on `obj.name`, `obj.type`, `obj.id` etc. against plain objects (e.g. `pg`'s prepared-statement naming check — a real incident at one consumer).**Persistence:** lasts until the Node process restarts, so the blast radius is *every* request that container handles after the pollution.Root causeIn `src/scimPatch.ts:415-427`, `addOrReplaceObjectAttribute` iterates the user-supplied `patch.value` with `Object.entries` and feeds each key to `resolvePaths`, which splits on `.`:`assign` then walks the resulting key path with no filtering on dangerous keys (`src/scimPatch.ts:437-445`):For `keyPath = ["__proto__", "polluted"]`:`"__proto__" in obj` is always true, so the fresh-object branch is skipped.`obj = obj["__proto__"]` now points to `Object.prototype`.The final write lands on `Object.prototype.polluted`.The same shape works for `constructor.prototype` keys.Proof of conceptDrop this in `test/prototypePollution.test.ts` and run `npm run build && npx mocha lib/test/prototypePollution.test.js`. Both tests pass against HEAD `871b1e2`:Suggested fixReject the three dangerous keys in `assign()` before the walk. Minimal patch:Alternative, slightly safer: switch the walk target to `Object.create(null)` nodes when creating intermediate objects, and use `Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true })` instead of `obj[key] = value` for the final write. That defends against future prototype-walking sinks even if a key sneaks past the denylist.Either approach is a non-breaking change — legitimate SCIM clients never send these keys.Mitigation for consumers who can't upgrade immediatelyCalling `Object.freeze(Object.prototype)` (and the same on `Array.prototype`, `Function.prototype`) at process startup neutralizes this class of bug — assignment to a frozen prototype becomes a silent no-op in sloppy mode or a `TypeError` in strict mode. Node's `--frozen-intrinsics` flag does this for built-ins automatically.CreditDiscovered by **Lee Wang (Notion)**. Reported by **David Wu (Notion)**.Report authored by **Claude**. Reviewed by **David Wu**.
CVSS v3.1 score: 9.1