VYPR
Critical severityNVD Advisory· Published Jun 23, 2026

Budibase has nonymous NoSQL operator injection via published-app query templates

CVE-2026-54350

Description

Summary

enrichContext at packages/server/src/sdk/workspace/queries/queries.ts:121-138 substitutes parameter values into the raw JSON body of a query, then JSON.parses the result. The validator validateQueryInputs at packages/server/src/api/controllers/query/index.ts:61-71 rejects only Handlebars markers ({{, }}) in user input and does not escape JSON metacharacters (", \, }). A parameter value containing a closing quote and additional keys lifts attacker-controlled fields into the parsed filter object.

For Mongo find, the parsed filter passes directly to collection.find() (packages/server/src/integrations/mongodb.ts:506-510). Duplicate-key JSON parsing overrides the builder's {name: "..."} with {name: {$exists: true}} and returns every document. The same primitive against an updateMany query (mongodb.ts:577-585) widens the filter scope to the full collection while the builder-controlled $set body runs against every matched document.

The authorized middleware at packages/server/src/middleware/authorized.ts:141-148 short-circuits when the query's role is PUBLIC. CSRF is not enforced on this path. POST /api/v2/queries/:queryId (packages/server/src/api/routes/query.ts:63) accepts the call with no session, only an x-budibase-app-id header that is public from the published-app URL.

Result: an unauthenticated visitor of any published Budibase app reads every document of the backing MongoDB, CouchDB, Elasticsearch, DynamoDB-PartiQL, or REST-with-JSON-body collection and, where the builder has published a PUBLIC write query, modifies every document of that collection with one HTTP request.

Affected

Budibase/budibase server, @budibase/server package, <= 3.39.0 (HEAD feab995, released 2026-05-20).

Reachable on any deployment where a workspace builder has set the role of a non-SQL query (MongoDB, CouchDB, Elasticsearch, DynamoDB-PartiQL, or REST with bodyType=json) to PUBLIC and published the app. This is the canonical low-code public-form use case.

SQL datasources (Postgres, MySQL, MSSQL, Oracle, MariaDB) route through interpolateSQL and are not affected.

Root cause

packages/server/src/sdk/workspace/queries/queries.ts:121-138: processStringSync(fields[key], parameters, {noEscaping: true, noHelpers: true}) writes the raw parameter value into the JSON-body string with no JSON-string escape; the followup JSON.parse(enrichedQuery.json || enrichedQuery.customData || enrichedQuery.requestBody) lifts the substituted text into the integration filter object.

packages/server/src/api/controllers/query/index.ts:61-71: validateQueryInputs only rejects values where findHBSBlocks(value).length !== 0 (Handlebars markers) and ignores JSON metacharacters.

packages/server/src/integrations/mongodb.ts:506-510: collection.find(json) receives the user-controlled filter object directly with no key prefix or operator allow-list.

packages/server/src/integrations/mongodb.ts:577-585: collection.updateMany(json.filter, json.update, json.options) accepts the templated filter without verifying that the substituted filter still matches the builder's intent.

packages/server/src/middleware/authorized.ts:141-148: if (resourceRoles.includes(roles.BUILTIN_ROLE_IDS.PUBLIC)) return next() skips both authentication and CSRF.

packages/server/src/integrations/queries/sql.ts:29-122: interpolateSQL rewrites every {{ binding }} to a positional bind placeholder ($N or ?). The SQL leg is bind-parameterised; the JSON leg is not.

Reproduction

budibase/budibase:latest (v3.39.0) Docker single-container, default config. Builder logs in once, creates a MongoDB datasource, creates a query GetUserByName with body { "name": "{{ name }}" }, sets the query role to PUBLIC, and publishes the app.

  1. Anonymous client sends the inject payload to the read query.
POST /api/v2/queries/ HTTP/1.1
Host: 
x-budibase-app-id: 
Content-Type: application/json

{"parameters":{"name":"x\",\"name\":{\"$exists\":true},\"$comment\":\"audit"}}
{"data":[
  {"_id":"...","name":"alice","secret":"alice-secret-flag"},
  {"_id":"...","name":"bob","secret":"bob-secret-flag"},
  {"_id":"...","name":"admin","role":"admin","secret":"ADMIN-SUPER-SECRET-FLAG"}
]}
  1. Builder publishes a second query TouchUser (verb update, action updateMany, body { "filter": { "name": "{{ name }}" }, "update": { "$set": { "touched": true } } }, role PUBLIC). Anonymous client sends the same inject pattern.
POST /api/v2/queries/ HTTP/1.1
Host: 
x-budibase-app-id: 
Content-Type: application/json

{"parameters":{"name":"x\",\"name\":{\"$exists\":true},\"$comment\":\"esc"}}
{"data":[{"acknowledged":true,"matchedCount":3,"modifiedCount":3,"upsertedId":null,"upsertedCount":0}]}

Live-verified: against Budibase v3.39.0 on 2026-05-20, anonymous read returned every document including ADMIN-SUPER-SECRET-FLAG; anonymous updateMany reported matchedCount: 3, modifiedCount: 3 against a 3-document collection where the builder's filter intended name = "x".

Impact

  • Anonymous read of every document in any backing MongoDB, CouchDB, Elasticsearch, DynamoDB-PartiQL, or REST-with-JSON-body collection reachable through a PUBLIC query, including columns the published query was not designed to return (password_hash, secret, api_token, mfa_secret).
  • Anonymous modification of every document of that collection where the builder has published a PUBLIC update, delete, or aggregate query, beyond the builder's intended single-document scope.
  • One HTTP request, no session, no CSRF, no user interaction.

Credit

Jan Kahmen, turingpoint (jan@turingpoint.de).

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@budibase/servernpm
< 3.39.123.39.12

Affected products

1

Patches

Vulnerability mechanics

Root cause

"Missing JSON-string escaping of parameter values before substitution into the query body allows an attacker to inject arbitrary MongoDB operators via duplicate-key JSON parsing."

Attack vector

An unauthenticated attacker sends a POST request to `/api/v2/queries/:queryId` with an `x-budibase-app-id` header (public from the published-app URL) and a crafted `parameters` JSON body. The parameter value contains a closing quote and additional MongoDB operators (e.g., `{"$exists":true}`), which, after string substitution and `JSON.parse`, overrides the builder's intended filter and returns every document in the collection [CWE-943]. The same technique against a `PUBLIC` write query (e.g., `updateMany`) widens the filter scope to the full collection while the builder-controlled `$set` body executes against every matched document. No session, CSRF token, or user interaction is required because the `authorized` middleware short-circuits for `PUBLIC` roles [ref_id=1].

Affected code

The vulnerability resides in `packages/server/src/sdk/workspace/queries/queries.ts:121-138` where `processStringSync` substitutes parameter values into the raw JSON body without JSON-string escaping, followed by `JSON.parse`. The validator `validateQueryInputs` at `packages/server/src/api/controllers/query/index.ts:61-71` only rejects Handlebars markers and ignores JSON metacharacters. For MongoDB, the parsed filter passes directly to `collection.find()` (`mongodb.ts:506-510`) or `collection.updateMany()` (`mongodb.ts:577-585`). The `authorized` middleware at `packages/server/src/middleware/authorized.ts:141-148` skips authentication when the query role is `PUBLIC`.

What the fix does

The advisory does not include a published patch, but the root cause is clear: `processStringSync` must JSON-string-escape parameter values before substitution, or the query body should use bind-parameter placeholders (as `interpolateSQL` does for SQL) instead of raw string interpolation [ref_id=1]. Additionally, `validateQueryInputs` must reject JSON metacharacters (`"`, `\`, `}`) in addition to Handlebars markers, and the MongoDB integration should validate that the final filter object only contains keys the builder intended. Until a fix is released, builders must avoid setting non-SQL query roles to `PUBLIC` on published apps.

Preconditions

  • configThe workspace builder must have set the role of a non-SQL query (MongoDB, CouchDB, Elasticsearch, DynamoDB-PartiQL, or REST with bodyType=json) to PUBLIC and published the app
  • inputThe attacker must know the published app ID (visible in the published-app URL) and the query ID
  • authNo authentication or CSRF token is required because the authorized middleware short-circuits for PUBLIC roles
  • networkThe attacker sends a single HTTP POST request to /api/v2/queries/:queryId

Reproduction

```http POST /api/v2/queries/<read-queryId> HTTP/1.1 Host: <budibase-host> x-budibase-app-id: <published-appId> Content-Type: application/json

{"parameters":{"name":"x\",\"name\":{\"$exists\":true},\"$comment\":\"audit"}} ```

Response returns every document in the collection, including fields like `secret` that the intended query was not designed to expose. For write queries, the same injection pattern against an `updateMany` query reports `matchedCount` and `modifiedCount` equal to the total document count [ref_id=1].

Generated on Jun 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.