
PoC para CVE-2026-54350 — Budibase inyección de operador NoSQL no autenticada (CVSS 10.0). Leer/escribir en masa cualquier colección de documentos mediante una consulta PUBLIC.
Inyección de operadores JSON/NoSQL no autenticada en Budibase. Cualquier visitante anónimo de una aplicación Budibase publicada que exponga una consulta PÚBLICA respaldada por una fuente de datos de documentos (MongoDB, CouchDB, Elasticsearch, DynamoDB-PartiQL o REST con un cuerpo JSON) puede leer todos los documentos de la colección subyacente y, si existe una consulta de escritura pública, modificar todos los documentos — con una sola solicitud HTTP no autenticada.
| CVE | CVE-2026-54350 |
| Aviso | 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 (Neutralización incorrecta de elementos especiales en una consulta de datos), CWE-89 |
| Autenticación | Ninguna (no autenticada) |
| Afectado | Budibase <= 3.39.0 (rango del aviso del proveedor) |
| Corregido | 3.39.12 según NVD; las comillas inyectadas ya están neutralizadas desde 3.39.1, y la corrección explícita (processJsonStringSync) llega en 3.39.9 |
| Verificación | CONFIRMADO — reproducido de extremo a extremo en 3.39.0 |
Al ejecutar una consulta, Budibase enriquece los parámetros del usuario en el cuerpo JSON sin procesar de la consulta con Handlebars, y luego analiza (JSON.parse) el resultado:
enrichContext() packages/server/src/sdk/workspace/queries/queries.ts
enrichedQuery.json = processStringSync('{"name":"{{ name }}"}', { name: <user> }, { noEscaping: true })
enrichedQuery.json = JSON.parse(enrichedQuery.json)
En las compilaciones afectadas, el parámetro se interpola sin escape JSON, por lo que un valor que contiene una " cierra la cadena prevista e inyecta claves hermanas en el objeto analizado. El único filtro de entrada, validateQueryInputs() (api/controllers/query/index.ts), rechaza solo los marcadores de Handlebars ({{, }}) — no toca ", \, } ni $.
Para un find de MongoDB, el objeto analizado se pasa directamente a collection.find() (integrations/mongodb.ts). Una clave name duplicada cuyo valor es un objeto operador gana la fusión de JSON.parse, convirtiendo una igualdad de cadena en {$exists: true} y devolviendo toda la colección. La misma primitiva contra una consulta updateMany amplía el filtro a cada documento.
El control de acceso se omite porque authorized() (middleware/authorized.ts:141) hace un cortocircuito con return next() cuando el rol del recurso de la consulta es PUBLIC, omitiendo tanto la autenticación de sesión como CSRF. El encabezado x-budibase-app-id necesario para llegar a la consulta es público — es parte de cada URL de aplicación publicada.
Vea ANALYSIS.md para el recorrido completo a nivel de código y la investigación de los límites de versión.
Plantilla del cuerpo de la consulta (según se configura en el constructor):
{"name":"{{ name }}"}
Valor inyectado para el parámetro name:
zzz","name":{"$exists":true},"$comment":"cve-2026-54350
Después de la interpolación y JSON.parse (fusión de claves duplicadas, gana la última):
{ "name": { "$exists": true }, "$comment": "cve-2026-54350" }
$comment es un meta-operador inerte de MongoDB que absorbe el "} final de la plantilla para que todo el cuerpo siga siendo JSON válido.
exploit.py — solo biblioteca estándar de Python 3, sin dependencias.
# 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 y --query-id son valores públicos observados en el tráfico API de la propia aplicación publicada. --field anula la clave JSON a la que está vinculado el parámetro (por defecto --param).
Requiere Docker. Este laboratorio usa --network host (no se asume red puente) y mongo:4.4 (el anfitrión no tiene AVX; MongoDB 5.0+ lo requiere).
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 solo utiliza APIs legítimas del constructor — no debilita ningún valor predeterminado. Marcar el rol de acceso de una consulta como PUBLIC es una característica de primera clase del constructor (las aplicaciones publicadas exponen datos exactamente a través de este mecanismo). Ejecución completa capturada en EVIDENCE.txt.
NVD enumera el rango afectado como < 3.39.12. Empíricamente, el vector de comillas simples solo es explotable en <= 3.39.0, que es exactamente el rango que indica el aviso GHSA del proveedor. A partir de 3.39.1, la capa de plantillas ya escapa las comillas inyectadas (verificado: la misma solicitud contra 3.39.8 es neutralizada), y 3.39.9 introduce la corrección explícita processJsonStringSync. Por lo tanto, este PoC apunta a 3.39.0, la última versión dentro del rango afectado del proveedor.
>= 3.39.12, o como mínimo >= 3.39.9 donde processJsonStringSync escapa con JSON los parámetros interpolados).PUBLIC; restríjalas a roles autenticados cuando sea posible.Busque solicitudes POST /api/v2/queries/<id>, de sesiones no autenticadas, cuyos valores de parameters contengan una " seguida de un operador MongoDB prefijado con $ ($exists, $ne, $gt, $where, $regex, ...) o una clave JSON duplicada. En MongoDB, active la creación de perfiles y alerte cuando un parámetro de consulta que normalmente es una cadena llegue como un objeto.
Investigación y PoC por Caio Fabrício (@BiiTts).