The vulnerability is a NoSQL injection in Budibase's MongoDB integration. It stems from the enrichContext function in packages/server/src/sdk/workspace/queries/queries.ts, which insecurely constructs MongoDB queries. The function used string substitution (processStringSync with noEscaping: true) to insert user-provided parameters into a JSON query string. This allowed an attacker to inject malicious MongoDB operators (e.g., $ne, $where) by crafting a special payload. The manipulated JSON is then parsed and passed to the MongoDB driver's find, updateOne, or deleteOne methods within the read, update, and delete functions in packages/server/src/integrations/mongodb.ts, leading to the execution of the malicious query. The patch addresses this by introducing a new function, processJsonStringSync, which safely handles JSON templates by escaping bindings within quoted strings, thus preventing the injection.
This function was vulnerable because it used `processStringSync` with the `noEscaping: true` option to substitute user-controlled parameters into a JSON string. This allowed an attacker to inject arbitrary JSON structures and MongoDB operators, leading to a NoSQL injection vulnerability. The patch replaces this with a call to `enrichJsonTemplate` which uses the safer `processJsonStringSync` for JSON fields.
read
packages/server/src/integrations/mongodb.ts
This function receives a query object that has been processed by `enrichContext`. It then passes the `json` field from this query object to the `collection.find()` method of the MongoDB driver. When the `json` object contains malicious operators injected via the `enrichContext` vulnerability, this function executes the malicious query, allowing an attacker to bypass access controls and read arbitrary data.
update
packages/server/src/integrations/mongodb.ts
According to the vulnerability description, `update` and `delete` action types are also affected. Similar to the `read` function, any function that takes the user-controlled query filter and applies it to an update or delete operation would be vulnerable to NoSQL injection, allowing an attacker to modify or delete arbitrary documents.
delete
packages/server/src/integrations/mongodb.ts
According to the vulnerability description, `update` and `delete` action types are also affected. Similar to the `read` function, any function that takes the user-controlled query filter and applies it to an update or delete operation would be vulnerable to NoSQL injection, allowing an attacker to modify or delete arbitrary documents.
createObjectIds walks the object and rewrites strings that look like ObjectId(...). It does not strip $-prefixed keys and does not reject operator-shaped values. The injected filter reaches collection.find unchanged.
For comparison, SQL datasources go through interpolateSQL at packages/server/src/threads/query.ts:145, which parameterizes bindings into driver-level bind variables. The MongoDB path has no equivalent.
Duplicate-key trick
The template gives the attacker one substitution point inside the name value. The outer {"name": " / "} is fixed. The attacker's payload:
JSON.parse keeps the last value for a duplicate key (ECMA-404 leaves this implementation-defined; V8 and Node's JSON.parse keep the last), so the parsed object is:
{ name: { $ne: "x" }, $comment: "bud-033" }
$comment is a MongoDB meta operator that the server accepts and ignores, so it consumes the template's trailing "} without restricting the query. The filter that reaches MongoDB is name != "x", which matches every document.
Proof of Concept
Tested against Budibase 3.35.8 (master at f960e361) and MongoDB 6.
Step 1: Admin creates a MongoDB datasource and seeds three documents:
Step 2: Alice, a builder, configures the datasource and writes a MongoDB query find-by-name whose json field is {"name": "{{name}}"}. She publishes the app and grants Bob (BASIC) a role on it.
Step 3: Bob, logged in as BASIC with a role on the published app, executes the query via the standard execute endpoint. POST /api/queries/:queryId is reachable by any app role with permission on the query (it is how Budibase renders query-backed tables to end users):
The builder's per-user filter collapses. A BASIC end-user who is only meant to read their own documents reads everyone's.
Concrete scenario: builder publishes an app whose "My records" screen runs a MongoDB query with json = {"email": "{{ currentUser.email }}"}. Each app user is supposed to see only the rows where email matches their session. Bob, a BASIC user in that app, sends bob@x.com", "email": {"$ne": "x"}, "$comment": "x as the currentUser.email binding replacement and receives every row in the collection, including other tenants' users, admin records, and any secret fields the builder stored alongside.
The blast radius depends on what the builder exposed:
Read queries: full-collection dump (demonstrated above: three docs returned where the scoped filter returned one, including other users' secrets).
$where operator: arbitrary JavaScript inside the MongoDB server process. The attacker exfiltrates any field of any document through the JS expression or via timing side channels.
$function / $accumulator (MongoDB 4.4+): arbitrary JS in aggregation stages.
$lookup: cross-collection joins within the same database. If the MongoDB datasource holds admin tokens or sensitive collections next to the one the builder queried, the injection reaches them.
update / delete action types: the filter injection rewrites the affected-document set. One request wipes or rewrites every document the connection can reach.
The blast radius is the builder's own MongoDB deployment, not Budibase infrastructure. Budibase does not ship or run MongoDB; this connector talks to the customer's external Mongo, so the attacker reads and writes data the builder's connection has access to and does not cross into Budibase's tenant boundary, CouchDB, MinIO, or Redis. The vulnerable pattern ({{binding}} inside the JSON body) is the exact shape Budibase's documentation shows for parameterized MongoDB queries, and there is no in-product warning that MongoDB behaves differently from SQL. CVSS reflects the common read-query scope (filter bypass on a single collection); the $where / $lookup / write-action paths exist but depend on what primitives the builder exposed.
Recommended Fix
Strip $-prefixed keys from any object that originates from user-controlled parameters before it reaches collection.find/updateOne/deleteOne. A single guard in createObjectIds covers the read, update, and delete paths:
// packages/server/src/integrations/mongodb.ts:394 (createObjectIds)
const DANGEROUS = new Set([
"$where", "$function", "$accumulator",
"$expr", "$regex", "$ne", "$nin", "$gt", "$gte", "$lt", "$lte",
"$or", "$and", "$nor", "$not", "$exists", "$type", "$mod",
"$text", "$comment",
])
function stripOperators(obj: any): any {
if (obj === null || typeof obj !== "object") return obj
if (Array.isArray(obj)) return obj.map(stripOperators)
const cleaned: Record<string, any> = {}
for (const [k, v] of Object.entries(obj)) {
if (DANGEROUS.has(k)) continue
cleaned[k] = stripOperators(v)
}
return cleaned
}
A safer fix mirrors the SQL path: introduce a MongoDB-aware enrichment that binds parameters as values instead of string-substituting them into the query JSON. That eliminates the whole class.