How to secure a SCIM endpoint: Lessons from CVE-2026-109758
Your SCIM endpoint is an admin API. A default bearer token and a PATCH path parser left Perforce's open to anyone who knew the token and, in one test, to running code. Here is what to check in your own.
On October 13, 2026, CVE-2026-109758 was published for Perforce Helix Authentication Service (HAS), the component that adds SSO and SCIM provisioning to Perforce's version control server. It is rated 9.3 (critical) under CVSS 4.0.
The bug is not exotic. According to the CVE record, HAS versions 2022.1 through 2025.2 shipped installation templates with a default SCIM bearer token, and the installer copied that value into the live configuration without replacing it. Anyone who knew the default and could reach the endpoint could create, update and deactivate users and groups.
The record then describes a chain. HAS used scim-patch, a popular npm library for applying SCIM PATCH operations, and affected releases of that library have a prototype pollution bug (CVE-2026-61834). A crafted PATCH request could corrupt application state and take down health checks, SCIM and SSO. In a later test, it was confirmed to enable remote code execution on a Node.js 24 and Express deployment.
Perforce removed the underlying defect in HAS 2026.1, released July 17, 2026. The record lists the CVE as not on CISA's Known Exploited Vulnerabilities catalog, and we found no reports of exploitation.
Few of our readers run HAS. Many of them run a SCIM endpoint they built themselves, and both halves of this bug are easy to reproduce in your own code. This post walks through each one and ends with a checklist.

Why does SCIM endpoint security matter?
SCIM endpoints tend to get treated as plumbing: a URL you hand to a customer's IT team once, which Okta or Entra ID then calls in the background. But look at what the endpoint can do. It creates accounts, deactivates them, and changes group memberships, which most B2B apps map straight to roles. A caller who can write to it can give themselves an account in any tenant the token covers, add that account to an admin group, or deactivate the people who would notice.
That is the same power as your internal admin panel, exposed on the public internet to a machine caller, usually guarded by one string.
The spec does not help much here. RFC 7644, the SCIM protocol, says plainly that it "does not itself define a SCIM-specific scheme for authentication and authorization." So every implementation picks its own, and most picked a long-lived bearer token. We covered why that default ages badly in Why SCIM needs OAuth 2.0 Client Credentials, not just bearer tokens.
How should a SCIM endpoint authenticate requests?
The first failure in the Perforce chain was a credential that shipped with the product. A default credential is the extreme case of a shared credential. Every install has the same one, so knowing it once means knowing it everywhere. The fix is not "remember to change it." It is to make the endpoint impossible to use until a unique credential exists.
What that looks like in practice:
- Generate tokens at setup, never in templates. No token value should exist in your code, config templates, Helm charts or docs. If the SCIM endpoint has no token configured, it should reject every request.
- One token per directory. Scope each token to a single customer directory, so a leaked token can only touch that tenant. A global SCIM token is a cross-tenant admin key.
- Show it once, store a hash. Treat the token like a password: display it at creation, store only a hash, and compare in constant time.
- Allow two active tokens during rotation. Rotation that requires downtime does not happen. Let the customer add a new token, update their IdP, then revoke the old one.
- Give tokens a recognizable prefix. It makes leaked tokens easy for secret scanners to find, and it makes the next point unambiguous.
- Accept the token with or without
Bearer. Okta's header token integrations sendAuthorization: {token}with no prefix. We documented this and other IdP quirks in What Okta and Entra ID actually send to your SCIM endpoint. - Offer OAuth 2.0 client credentials where the IdP supports it, so the token on the wire is short-lived.
A minimal version of the token check in Node.js:
Note what is missing: there is no fallback. A directory with no tokens rejects everything.
How do you validate SCIM PATCH paths safely?
The second failure is treating PATCH paths as trusted input. It is less obvious, and it applies to almost any SCIM server written in JavaScript.
A SCIM PATCH request carries a list of operations. Each operation has an op, an optional path such as name.givenName, and a value. Per RFC 7644, path is optional for add and replace; when it is omitted, value is an object whose keys are attribute names. Either way, the IdP decides which attribute names your server will walk, and in practice those names arrive as dotted strings. Entra ID, for example, sends dotted sub-attribute names and full extension URNs as keys inside a path-less replace when its SCIM compliance flag is turned on.
The natural way to apply a dotted path is to split it and walk the object. Here is a version that looks careful, because it blocks the three names everyone knows to block:
The in operator checks inherited properties too. toString is not on user, but it is on Object.prototype, so the loop happily walks into the shared Object.prototype.toString function and writes to it. Every object in the process now sees the attacker's property.
This is the pattern in the scim-patch advisory. The library blocked __proto__, constructor and prototype, but its navigate() and assign() functions still followed inherited properties, so paths like toString.x got through. The same advisory notes that a path-less operation with a dotted key in value is affected as well. On its own the library bug is rated moderate (CVSS 3.1 4.3), because the write is narrower than full Object.prototype pollution. Behind a default token, and next to gadgets like template engines, it became part of a critical chain. It is fixed in scim-patch 0.9.2.
The fix has two parts. Only follow properties the object owns, and only accept paths your schema defines:
The allowlist does most of the work. A SCIM server already knows its schema, so there is no reason to apply a path it does not recognize. The RFC gives you the right error for it: 400 with scimType of invalidPath. Applying unknown attributes "just in case" is how a provisioning API becomes a general-purpose object writer.
If you use a library to apply PATCH operations, check its version today, and check whether it validates paths against a schema or walks whatever it is given.
What limits the damage if a SCIM token leaks?
The CVE record lists three conditions for the first half of the attack: the default token is still in place, the SCIM endpoint is reachable, and provisioning is configured against a backend. Each one is a place to cut the chain.
- Limit what SCIM can grant. Map IdP groups to roles explicitly, per organization, and decide whether a SCIM group should ever be able to grant your app's highest admin role without a human approving the mapping.
- Log every SCIM write with the directory, the token or client that made it, and the before and after values. After an incident like this one, the question is "which users and groups changed that no IdP sync explains," and you can only answer it if you recorded the changes.
- Restrict network reachability where you can. If the SCIM endpoint shares a process with your SSO handler and your template engine, a bug in one is a bug in all of them. Isolating it limits what a parser bug can reach.
SCIM endpoint security checklist
Can you avoid running a SCIM endpoint yourself?
With WorkOS Directory Sync, WorkOS hosts the SCIM endpoint for each customer's directory. Each directory gets its own credentials: a bearer token generated in the Dashboard, or OAuth 2.0 client credentials with a per-directory token URL, which Okta and Entra ID both support. Your customer's IT contact can generate, rotate and revoke bearer tokens from the Admin Portal, and a directory can hold two active client secrets so rotation does not interrupt the sync. Your app receives the same dsync.* events whichever IdP is on the other side.
You still own what happens after the event arrives, including how groups map to roles. But parsing PATCH operations from every IdP, and keeping the credentials that guard them, stops being your code.