Skip to content

Pre-Auth NoSQL Injection in CAS Login Handler leading to Arbitrary CAS/SAML User Session Hijack

Critical
julio-rocketchat published GHSA-rr54-jf4h-6cj9 May 14, 2026

Package

Rocket.Chat

Affected versions

<8.5.0, <8.4.1, <8.3.3, <8.2.3, <8.1.4, <8.0.5, <7.13.7, <7.10.11

Patched versions

8.5.0, 8.4.1, 8.3.3, 8.2.3, 8.1.4, 8.0.5, 7.13.7, 7.10.11

Description

Rocket.Chat's CAS login handler forwards the client-supplied options.cas.credentialToken value straight into a MongoDB findOne({_id: ...}) query without any runtime type check. TypeScript's string parameter annotation is erased at runtime, so an unauthenticated attacker can substitute a MongoDB query operator ({"$gt": ""}, {"$ne": null}, etc.) for what the server expects to be an opaque ticket string. The injected operator matches the first unexpired document in the credential_tokens collection, bypassing the CAS ticket check entirely.

When any legitimate CAS or SAML SSO login is in flight, the attacker's next DDP login call matches the same credential-token row via the NoSQL operator and is issued a full Meteor auth token (userId + token) bound to the victim. The token is immediately usable against the complete REST and DDP surface as that user. If the victim is an administrator, this escalates to full instance compromise via Apps-Engine app install.

Because the credential_tokens collection is populated by both the CAS middleware and the SAML service, and because the vulnerable CAS handler is registered unconditionally at startup regardless of whether CAS is actually enabled, any Rocket.Chat instance with either SSO provider configured is exploitable.

Pre-requisites

  • No account needed. Attacker is fully unauthenticated; they only need network reach to the instance's /websocket (Meteor DDP) or /api/v1/login endpoint.
  • The target instance must have CAS or SAML configured and actively in use. Both providers write into the same credential_tokens collection that the CAS handler reads, so a SAML-only deployment is exploitable through the CAS handler's NoSQL primitive too.
  • A legitimate SSO login must occur (or have occurred) within the 60-second credential-token window (CredentialTokens.ts:17, hardcoded validForMilliseconds = 60000). On an SSO-backed instance any user logging in creates that window. An attacker polling at sub-second intervals wins the race deterministically when a row appears.

Root Cause

1. No type validation on credentialToken in the CAS handler:
apps/meteor/server/lib/cas/loginHandler.ts#L12-L21:

export const loginHandlerCAS = async (options: any): Promise<...> => {
    if (!options.cas) {
        return undefined;
    }

    const credentials = await CredentialTokens.findOneNotExpiredById(options.cas.credentialToken);
    if (credentials === undefined || credentials === null) {
        throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching login attempt found');
    }
    ...

options is typed any and options.cas.credentialToken is passed straight to the model at line 18. Compare with the SAML sibling handler at app/meteor-accounts-saml/server/loginHandler.ts#L14-L17, which has the exact guard the CAS handler lacks:

Accounts.registerLoginHandler('saml', async (loginRequest) => {
    if (!loginRequest.saml || !loginRequest.credentialToken || typeof loginRequest.credentialToken !== 'string') {
        return undefined;
    }
    ...

The guard on the SAML handler prevents the equivalent operator-object injection on that path; the CAS handler has no corresponding check.

2. Raw query composition in the model:
packages/models/src/models/CredentialTokens.ts#L28-L35:

findOneNotExpiredById(_id: string): Promise<ICredentialToken | null> {
    const query = {
        _id,
        expireAt: { $gt: new Date() },
    };

    return this.findOne(query);
}

The _id: string annotation is compile-time only. Whatever the handler passes - string or object - is placed into the query document as-is. {_id: {$gt: ""}} is a valid Mongo query that matches every document with any non-empty _id (i.e. every row in the collection), combined with the existing expireAt: {$gt: new Date()} filter, it matches any currently-valid credential token.

3. CAS handler registered unconditionally at startup:
apps/meteor/server/configuration/cas.ts#L30-L36:

Accounts.registerLoginHandler('cas', (options) => {
    const promise = loginHandlerCAS(options);
    return promise as unknown as Awaited<typeof promise>;
});

configureCAS runs at server startup with no CAS_enabled gate. The DDP login method accepts {cas: {...}} payloads on every Rocket.Chat instance, and the vulnerable handler is reached regardless of whether CAS has ever been configured in the admin UI. The only thing standing between the attacker and a hit is a non-empty credential_tokens collection.

4. Cross-provider attack surface via SAML writes into the same collection:
apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts#L99-L101:

public static async storeCredential(credentialToken: string, loginResult: { profile: Record<string, any> }): Promise<void> {
    await CredentialTokens.create(credentialToken, loginResult);
}

SAML and CAS share the same credential_tokens collection. The vulnerable CAS handler will happily return SAML-populated rows to an attacker injecting through the cas login path. This widens the affected population from "CAS-enabled instances" to "CAS-enabled or SAML-enabled instances" - the latter being substantially larger in enterprise deployments.

5. Short but deterministic race window:
packages/models/src/models/CredentialTokens.ts#L16-L26:

async create(_id: string, userInfo: ICredentialToken['userInfo']): Promise<ICredentialToken> {
    const validForMilliseconds = 60000; // Valid for 60 seconds
    const token = {
        _id,
        userInfo,
        expireAt: new Date(Date.now() + validForMilliseconds),
    };
    await this.insertOne(token);
    return token;
}

The CAS handler does not delete the row on successful read - it only filters on expireAt. So for the full 60 seconds after an SSO callback completes, the row remains in the collection and is injectable on any $gt:"" poll. Mongo's TTL index on expireAt sweeps after expiry but the 60-second window is deterministic for the attacker.

Impact

  • Full account takeover of any user who performs CAS or SAML SSO login while the attacker is polling. Stolen authToken is a first-class Meteor session token: usable via X-Auth-Token / X-User-Id headers against /api/v1/*, and via DDP login with {resume: token} for full WebSocket access.
  • Admin escalation if the victim is an admin: attacker installs an arbitrary Apps-Engine app for server-side code execution, modifies settings, exfiltrates every user's data.
  • Cross-provider blast radius: SAML-only instances (which do not use CAS at all) are still exploitable because the CAS handler cross-reads the collection SAML writes into. Operators who disabled CAS long ago remain exposed.
  • No auth, no account, no user interaction with the attacker. The attacker never touches the IdP; they only hit Rocket.Chat's own /websocket.
  • Silent to the victim: the victim's own CAS login completes normally (the handler does not delete the credential-token row on read), so the attack leaves no user-visible trace. Server-side DDP access logs will record an extra login method call per hijack whose params object contains the operator form {cas:{credentialToken:{"$gt":""}}} - this is distinguishable from legitimate client traffic (which sends a string credentialToken) and a log reviewer can detect the pattern with a grep for "$gt" or equivalent in the method: "login" payloads, but by default RC does not log DDP method bodies, so nothing on disk captures the operator object without extra instrumentation.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

CVE ID

CVE-2026-45688

Weaknesses

Improper Neutralization of Special Elements in Data Query Logic

The product generates a query intended to access or manipulate data in a data store such as a database, but it does not neutralize or incorrectly neutralizes special elements that can modify the intended logic of the query. Learn more on MITRE.

Credits