GHSA-8qv3-p479-cj62
CRITICALGHSA-8qv3-p479-cj62 is a critical-severity (CVSS 10) SQL Injection vulnerability in @budibase/server. O3 Security confirms whether GHSA-8qv3-p479-cj62 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Budibase has nonymous NoSQL operator injection via published-app query templates
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-8qv3-p479-cj62.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-8qv3-p479-cj62 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 369,023 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
@budibase/servernpmDescription
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.
- Anonymous client sends the inject payload to the read query.
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"}}
{"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"}
]}
- Builder publishes a second query
TouchUser(verbupdate, actionupdateMany, body{ "filter": { "name": "{{ name }}" }, "update": { "$set": { "touched": true } } }, rolePUBLIC). Anonymous client sends the same inject pattern.
POST /api/v2/queries/<updateMany-queryId> HTTP/1.1
Host: <budibase-host>
x-budibase-app-id: <published-appId>
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
PUBLICquery, 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
PUBLICupdate,delete, oraggregatequery, beyond the builder's intended single-document scope. - One HTTP request, no session, no CSRF, no user interaction.
Credit
Jan Kahmen, turingpoint ([email protected]).
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | @budibase/server | all versions | 3.39.12 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @budibase/server. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update @budibase/server to 3.39.12 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-8qv3-p479-cj62 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-8qv3-p479-cj62 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-8qv3-p479-cj62. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-8qv3-p479-cj62 in your dependencies?
O3 detects GHSA-8qv3-p479-cj62 across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.