Feb 10, 2026·6 min read·8 visits
An authenticated attacker can escalate privileges by injecting a `securityContext` object into WebSocket messages or triggering a race condition in the API gateway. This allows bypassing Row-Level Security (RLS) and multi-tenant isolation, granting full admin access or data visibility across tenants.
Cube (formerly Cube.js) is the self-proclaimed 'semantic layer' for building data applications—a fancy way of saying it sits between your messy SQL databases and your pretty frontend charts. In versions prior to 1.5.13, Cube suffered from a catastrophic logic error in its WebSocket gateway and async handling. By simply asking nicely (read: injecting a JSON object), a low-privileged user could override their own security context. Even worse, a race condition in the promise chain allowed user contexts to 'bleed' into one another, meaning your request for 'My Sales' might accidentally return 'competitor_sales' if the server was busy enough. This is a classic case of trusting client input combined with the distinct misery of Node.js event loop management.
In modern data stacks, 'Semantic Layers' are the new hotness. They act as the single source of truth, translating abstract business logic (like 'Revenue') into concrete SQL queries. Cube is the leader in this space. It handles authentication, caching, and query orchestration. Ideally, it's the bouncer at the club, checking IDs and ensuring you only sit at the table you paid for.
But here's the thing about bouncers: if they are distracted or easily bribed, the whole security model collapses. Cube relies heavily on 'Security Contexts'—objects defining who you are and what you can see (e.g., tenant_id: 123). This context is usually derived from a cryptographically signed JWT. You trust the JWT, you trust the context.
CVE-2026-25958 reveals that Cube didn't just trust the JWT. It also trusted the WebSocket packet you just sent it. It’s the equivalent of the bouncer checking your ID, then asking, 'And what VIP level would you like to be today?' and actually writing it down on your wristband.
This vulnerability is a two-headed beast. The first head is WebSocket Parameter Pollution. When you connect to Cube via WebSockets, you send JSON messages to trigger data loads. The backend logic took the user's provided message and merged it into the internal state. The developers failed to sanitize the input for reserved keys like securityContext.
If you sent a payload containing securityContext: { isAdmin: true }, the application effectively said, 'Sure, seems legit,' and merged that flag into your active session. This allows for trivial privilege escalation. You aren't cracking a password; you're just politely informing the server that you are, in fact, the boss.
The second head is a Race Condition in the Async Boundary. In packages/cubejs-api-gateway/src/gateway.ts, specific core methods (like meta and sql) invoked response callbacks without properly awaiting the result. In the Node.js event loop, this created a 'broken promise chain'.
Because the request context wasn't strictly bound to the execution scope of the database query, high-concurrency traffic could cause Context Bleeding. The security context of User A (an Admin) could leak into the response processing of User B (a Viewer), accidentally serving privileged data to the wrong socket.
Let's look at the failure in the WebSocket handler. The code was iterating over the message parameters and blindly applying them. It lacked a 'strict' schema validation.
The Vulnerable Pattern (Simplified):
// Inside the WebSocket message handler
const processMessage = (message, connection) => {
// message.params comes directly from the user
const queryContext = {
...connection.userContext, // The real context from JWT
...message.params // The untrusted input overwriting it
};
// queryContext now has user-injected 'isAdmin: true'
return executeQuery(queryContext);
};The Fix (Commit 6271520):
The patch introduced zod schemas to strictly validate inputs. Instead of a spread operator that merges everything, the new code explicitly parses only allowed fields.
// The Fix: Strict Schema Validation
const LoadRequestSchema = z.object({
query: QuerySchema,
queryType: z.string().optional(),
// securityContext is notably ABSENT from the allowed schema
});
const safeParams = LoadRequestSchema.parse(message.params);
// Now safeParams only contains what the devs intended.Additionally, in gateway.ts (Commit 9e9f9d1), they had to fix the async bleeding.
Before:
this.loadImpl(options, (err, res) => {
// Callback hell leading to context loss
});After:
// Forcing the promise to resolve within the correct context scope
await this.loadImpl(options);By forcing the await, the AsyncLocalStorage context remains consistent throughout the lifecycle of the request.
To exploit this, you don't need fancy binary tools. You just need a WebSocket client and a valid low-privileged JWT. The goal is to bypass the Row-Level Security (RLS) that keeps tenants separate.
Step 1: Connect Establish a standard WebSocket connection to the Cube API. Authenticate with your low-privilege JWT.
Step 2: Craft the Payload
Construct a load message. The documentation says you should send a query. The exploit says you should send a query AND a securityContext.
{
"messageId": "1337",
"method": "load",
"params": {
"query": {
"measures": ["Orders.totalRevenue"],
"dimensions": ["Orders.createdAt"]
},
"securityContext": {
"u": {
"id": 1,
"roles": ["admin"],
"tenantId": "*"
}
}
}
}Step 3: Execution
Send the JSON. The server deserializes the message. The securityContext provided in params overrides the one extracted from your JWT. Cube generates the SQL. Instead of WHERE tenant_id = 'your_id', it generates WHERE 1=1 (or whatever the admin rule is).
Step 4: Profit The server streams back the financial data for every tenant on the platform. You have successfully democratized the data, whether they wanted you to or not.
This is a Critical Severity issue for any multi-tenant SaaS application using Cube. The impact is not just data leakage; it's total isolation failure.
order parameter (CWE-89), this could escalate further into blind SQL injection via the metadata generation layer.The business impact? If you are a B2B SaaS provider, your customers' competitors just got access to their sales numbers. Trust is the first casualty.
Remediation is straightforward but urgent. You must upgrade the Cube core packages.
Action Items:
1.5.13, 1.4.2, or 1.0.14 immediately. These versions introduce the Zod schema validation that strips the malicious payload.securityContext key in the parameters. This key should effectively never come from the client side.CUBEJS_WEB_SOCKETS=false. This reduces the attack surface significantly, as the REST API uses a different parsing path that was less susceptible to this specific pollution attack.Developer Lesson: Never use the spread operator (...) on user input when building security contexts. It's concise, it's pretty, and it's dangerous.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Cube Cube Dev | 0.27.19 - < 1.0.14 | 1.0.14 |
Cube Cube Dev | 1.1.0 - < 1.4.2 | 1.4.2 |
Cube Cube Dev | 1.5.0 - < 1.5.13 | 1.5.13 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-807 |
| Attack Vector | Network (WebSocket) |
| CVSS | 7.7 (High) |
| Impact | Privilege Escalation / Info Disclosure |
| Exploit Status | PoC Available |
| Fix Complexity | Low (Version Upgrade) |
The application relies on untrusted inputs to make security decisions, specifically allowing client-side overriding of the security context.