Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2026-19264 — CVE-2026-19264 - Critical unauthenticated path traversal to full instance takeover in Postiz (< 2.22.1). Technical writeup: decode-order bypass, JWT_SECRET escalation, and analysis of the upstream fix. | Kitploit
أدوات/GitHubGitHub/darklycn1976/cve-2026-19264
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationWeb SecurityLearning & Education
GitHubdarklycn1976/cve-2026-19264

CVE-2026-19264

CVE-2026-19264 - Critical unauthenticated path traversal to full instance takeover in Postiz (< 2.22.1). Technical writeup: decode-order bypass, JWT_SECRET escalation, and analysis of the upstream fix.

عرض المستودع
منذ 10 أياملم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة
الموقع الإلكتروني

CVE-2026-19264 - اجتياز مسار غير مصادَق إلى السيطرة الكاملة على مثيل في Postiz

CVE-2026-19264 - اجتياز مسار غير مصادَق إلى السيطرة الكاملة على مثيل في Postiz

المؤلف: Krithik Babu P (@DarkLycn1976) نُشر: 2026-08-10 CVE: CVE-2026-19264 الخطورة: حرِج - CVSS 4.0 9.3 / CVSS 3.1 9.8 CWE: CWE-22 - تقييد غير سليم لاسم المسار إلى دليل مقيد متأثر: gitroomhq/postiz-app < 2.22.1 أُصلح في: v2.22.1


الخلاصة

يقدّم Postiz الوسائط المخزّنة محليًا عبر مسار يضمّ مقاطع المسار المورّدة في عنوان URL إلى دليل الرفع ثم يبث النتيجة - دون أي تطبيع للمسار، أو فحص احتواء، أو مصادقة.

يُرجع حمولة الاجتياز الواضحة 404، لأن Next.js يطوي مقاطع ../ قبل التوجيه. لكن فواصل الترميز بعنوان URL تنجو من مطابقة المسار وتُفك ترميزها مرة واحدة إضافية بالضبط في الطريق إلى استدعاء نظام الملفات، مما يعيد الاجتياز إلى الجانب الآخر من كل فحص.

يمكن لمهاجم غير مصادَق قراءة أي ملف يمكن لعملية التطبيق قراءته - بما في ذلك بيئته الخاصة، التي تحمل سر توقيع JWT. ولأن Postiz يوقّع رموز الجلسة بذلك السر ويصدرها بدون مطالبة انتهاء الصلاحية، فإن استرجاعه يحوّل بدائية قراءة الملفات إلى جلسة دائمة قابلة للتزوير بأي مستخدم، بما في ذلك المسؤول.

طلب GET واحد غير مصادَق يؤدي إلى السيطرة الكاملة على المثيل.


1. الخلفية

Postiz هو منصة جدولة وسائل تواصل اجتماعي مفتوحة المصدر - نحو 34,000 نجمة على GitHub وقت كتابة هذا التقرير - مبنيّة بواجهة أمامية Next.js وخلفية NestJS. تستضيفه ذاتيًا وكالات وفرق صغيرة على نطاق واسع لإدارة حسابات التواصل الاجتماعي المتصلة والمحتوى المجدول والفواتير.

يمكن للنشر الذاتي تخزين الوسائط المرفوعة محليًا بدلًا من التخزين الكائني. يتحكم في هذا السلوك متغير بيئة واحد:

root@kitploit:~
STORAGE_PROVIDER=local

هذه هي القيمة المضمّنة في .env.example، لذا فهي ما يشغّله معظم المستضيفين ذاتيًا ما لم يكوّنوا S3 أو Cloudflare R2 عمدًا.

2. سطح الهجوم

عندما يكون التخزين المحلي مفعّلًا، يعيد next.config.js كتابة المسار العام /uploads/:path* إلى مسار API داخلي:

root@kitploit:~
apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts

خاصيتان تجعلان هذا المسار مثيرًا للاهتمام قبل أي ثغرة:

  1. إنه غير مصادَق. لا يحرسه أي برمجية وسيطة أمامية. خدمة الوسائط العامة هي القصد، لذا لا يُشترط أي جلسة.
  2. إنه مسار التقاط-الكل (catch-all). مقطع الالتقاط الاختياري [[...path]] يعني أن كل مقطع مسار متبقٍ يصل كمصفوفة يمكن للمعالج تفسيرها بحرية.

عندما يكون STORAGE_PROVIDER أي قيمة غير local، تشير إعادة الكتابة إلى /404 ويصبح المعالج غير قابل للوصول. بوابة التكوين هذه هي الشيء الوحيد الذي يفصل بين النشر وهذه الثغرة.

3. الكود المعرض للثغرة

المعالج، قبل الإصدار v2.22.1:

root@kitploit:~
export const GET = async (request: NextRequest, context) => {
  const { path } = await context.params;
  const filePath =
    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');

  const response  = createReadStream(filePath);
  const fileStats = statSync(filePath);
  // ... stream the file back to the caller
};

ثلاثة عيوب في أربعة أسطر:

  • لا تطبيع. path.normalize()، path.resolve() - لا يُستدعى أي منهما. المقاطع الواصلة تُضم حرفيًا كما وصلت.
  • لا فحص احتواء. لا شيء يتحقق من أن filePath الناتج ما يزال داخل UPLOAD_DIRECTORY.
  • ضم سلاسل نصية، لا ضم مسارات. + '/' + يعامل المكونات كنص، لا كمسار له دلالات.

النتيجة تذهب مباشرة إلى createReadStream() وتُبث البايتات إلى المتصل بنوع MIME يُستنتج من اسم الملف. لا توجد قائمة سماح بالامتدادات ولا مرشح محتوى.

4. لماذا تفشل الحمولة الواضحة

الهجوم النموذجي هو:

root@kitploit:~
GET /uploads/../../../etc/passwd

يُرجع هذا 404 على Postiz، وهذا الـ 404 هو السبب الكامل وراء بقاء هذه الثغرة حتى اكتُشفت.

يطبّع Next.js مسار الطلب أثناء التوجيه. تُطوى مقاطع ../ الخام قبل أن يقرر الموجّه أي معالج يستدعي. بحلول وصول الطلب إلى مسار الالتقاط، يكون الاجتياز قد أُزيل بالفعل - إما أن المسار يُحل إلى مكان لا يوجد فيه مسار مطابق، أو يُحل إلى داخل /uploads مع اختفاء المقاطع المنقوطة.

بالنسبة لمن يختبر بسرعة، يقرأ ذلك الـ 404 كأنه "الإطار يعالج هذا". إنه دفاع حقيقي وفعّال. المشكلة ليست في غيابه - بل في مكان تنفيذه في خط الأنابيب.

5. التجاوز - عدم تطابق ترتيب فك الترميز

لا تؤدي مطابقة المسار ومعالج الطلب نفس عدد مرات فك ترميز النسبة المئوية.

إذا كانت الفواصل مشفّرة بالنسبة المئوية، فإن التسلسل ليس فاصل مسار أثناء مطابقة المسار. %2e%2e%2f مجرد سلسلة معتمة - نص خامل ليس لدى المطبّع سبب للمسه. يمر عبر التوجيه سليمًا، يطابقه مسار الالتقاط، ويُفك ترميزه في طريقه إلى params الخاصة بالمعالج، حيث يصبح ../ من جديد.

عند تلك النقطة يُضم إلى UPLOAD_DIRECTORY ويُسلَّم إلى createReadStream() - متجاوزًا التوجيه والتطبيع وكل ضابط كان سيمنعه.

أشكال مجدية:

root@kitploit:~
GET /uploads/%2e%2e%2fsecretdir%2fsecret.txt        → 200, ملف خارج دليل الرفع
GET /uploads/..%2f..%2f..%2fetc%2fpasswd            → 200
GET /uploads/%2e%2e%2f%2e%2e%2f...%2fetc%2fpasswd   → 200, أعاد ملف /etc/passwd الفعلي

الترميز المضاعف لا ينجح - يبقى %252e حرفيًا عبر مرحلة فك الترميز الوحيدة ولا يصبح نقطة أبدًا. طبقة واحدة بالضبط من الترميز هي النقطة المثلى، وهو تذكير مفيد بأن "شدد الترميز أكثر" ليس استراتيجية.

الثابت الذي يجب التمسك به:

الضابط الذي يعمل قبل اكتمال فك الترميز لا يحمي نقطة الوصول (sink).

6. التصعيد - من القراءة العشوائية إلى السيطرة على المثيل

بدائية قراءة الملفات هي ثغرة عالية بحد ذاتها. ما يجعلها حرجة هو ما تصل إليه.

الخطوة 1 - قراءة البيئة. إعدادات عملية Node نفسها موجودة على القرص في جذر النشر. يوفّر .env، من بين أمور أخرى:

  • JWT_SECRET - مفتاح توقيع رموز الجلسة
  • DATABASE_URL - بيانات اعتماد Postgres كاملة
  • أسرار OAuth للمزود المتصل ومفاتيح الفوترة

الخطوة 2 - تزوير جلسة. يوقّع Postiz رموز الجلسة بـ JWT_SECRET باستخدام HS256 عبر jsonwebtoken. والأهم أن الرموز تُصدر بدون expiresIn، لذا يكون الرمز المزوّر صالحًا إلى أجل غير مسمى.

الخطوة 3 - كن أي شخص. يعيد برمجية المصادقة الوسيطة حل المستخدم من قاعدة البيانات باستخدام مطالبة id. وهي عمدًا لا تثق بمطالبة مثل isSuperAdmin من الرمز - تصميم جيد - لكن هذا التحصين غير ذي صلة بمجرد أن تتمكن من توقيع id عشوائي. توقيع { id: <معرف المستخدم الضحية> } ينتج جلسة لا يمكن تمييزها عن تسجيل دخول مشروع:

root@kitploit:~
read .env  →  JWT_SECRET  →  sign({ id: victim })  →  authenticated as victim, forever

تحققت من هذا مقابل منطق التحقق الحقيقي للمشروع مع اعتماد jsonwebtoken الفعلي: قُبل رمز موقّع بالسر المسترجع، ورُفض رمز موقّع بسر خاطئ. حالة التحكم مهمة - بدونها لديك افتراض، لا اكتشاف.

الخطوة 4 - المسار الموازي. DATABASE_URL وحده كافٍ للوصول المباشر إلى Postgres: اقرأ كل حساب متصل، أو اقلب علامة المسؤول مباشرة.

لا كلمة مرور. لا وصول سابق. لا تفاعل مستخدم. طلب HTTP واحد غير مصادَق.

7. الأثر والتقييم

root@kitploit:~
CVSS 4.0  9.3  AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS 3.1  9.8  AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

PR:N وUI:N هما المقياسان اللذان يقومان بالعمل. المسار لا يتطلب جلسة ولا تفاعل ضحية - المهاجم يعمل وحده، عبر الشبكة، ضد تكوين افتراضي.

الحد الوحيد الصادق هو بوابة التكوين: النشر على S3 أو R2 غير معرض، لأن المسار يُعاد كتابته إلى /404. هذا يقلل عدد المتأثرين لكنه لا يقلل الخطورة لأي شخص داخلهم - وlocal هي الافتراضي المُسلَّم.

8. الإصلاح

تصحيح المطوّرين (7936062) ثمانية أسطر ويستحق القراءة، لأنه صحيح بطريقة لا تكون عليها هذه الإصلاحات غالبًا:

root@kitploit:~
+import { resolve, sep } from 'path';
...
-  const filePath =
-    process.env.UPLOAD_DIRECTORY + '/' + (path ?? []).join('/');
+  const base = resolve(process.env.UPLOAD_DIRECTORY!);
+  const filePath = resolve(base, (path ?? []).join('/'));
+  // Confine reads to UPLOAD_DIRECTORY. resolve() collapses any `..` segments
+  // (including URL-decoded ones), so this blocks every path-traversal variant.
+  if (filePath !== base && !filePath.startsWith(base + sep)) {
+    return new NextResponse('Not found', { status: 404 });
+  }

شيئان فعلهما بشكل صحيح:

  1. يطبّع عند نقطة الوصول (sink). resolve() يطوي .. بعد اكتمال فك الترميز، لذا لا يهم كيف هُرّب الاجتياز عبر التوجيه. الفحص الآن في مكان الخطر.
  2. يقارن بـ base + sep، لا بـ base. قبول filePath.startsWith(base) ساذجًا كان سيقبل /app/uploads-evil/x كأنه داخل /app/uploads - تجاوز كلاسيكي لمطابقة البادئة. إلحاق الفاصل يسدّ هذا الثغر، وجملة filePath !== base تحافظ على صلاحية الدليل نفسه.

هذا هو الشكل الصحيح لفحص الاحتواء: حلّ، ثم قارن بالقاعدة مع فاصل لاحق.

9. الجدول الزمني للإفصاح

كل الأوقات UTC، 2026-07-20 ما لم يُذكر خلاف ذلك.

الوقتالحدث
05:55الإبلاغ عن الاستشارة الأمنية لفريق Postiz
07:44أقرّ المطوّرون بالثغرة وتحققوا منها

ست ساعات وثلاث وعشرون دقيقة من التقرير إلى إصدار التصحيح، على مشروع مفتوح المصدر بلا برنامج مكافآت مرتبط. رأيت تقارير تبقى دون معالجة لأشهر في مؤسسات لديها فرق أمنية مخصصة. الفضل لـ Enno Gelhaus للتنسيق وNevo David للمعالجة.

10. الدروس المستفادة

اجتياز ضابط لاختبارك لا يعني أن الضابط في المكان الصحيح. الـ 404 كان حقيقيًا. Next.js يطوي ../ فعلًا. الدفاع ببساطة عمل قبل اكتمال فك ترميز المدخلات، ما يعني أنه كان يحرس الموجّه لا استدعاء نظام الملفات. عندما تجد تخفيفًا، اسأل متى ينفَّذ بالنسبة لنقطة الوصول (sink) - ليس فقط إن كان موجودًا.

الترميز طبقة، والطبقات تُقشر بمعدلات مختلفة. كلما اختلف مكوّنان في خط أنابيب الطلب حول عدد مرات فك الترميز، كانت الفجوة بينهما قابلة للاستغلال. مطابقو المسارات والبرمجيات الوسيطة والمعالجات يختلفون غالبًا.

قيّم بدائية قراءة الملفات بما يمكن للعملية الوصول إليه، لا بالبدائية نفسها. "قراءة ملفات عشوائية" تبدو ككشف معلومات. أصبحت حرجة لأن البيئة كانت قابلة للقراءة، والسر فيها يوقّع الجلسات، وتلك الجلسات لا تنتهي أبدًا. اتبع السلسلة قبل تقييمها.

الرموز غير المنتهية تحوّل التسريب إلى اختراق دائم. كشف مفتاح توقيع مع رموز قصيرة العمر هو يوم سيء. بدون expiresIn، لا يمكن الاسترداد دون تدوير السر - ومعظم المشغلين لن يعرفوا أبدًا أنهم بحاجة إلى ذلك.

شغّل حالة التحكم. التحقق من أن رمزًا موقّعًا بالسر الخاطئ مرفوض هو ما يفصل اكتشافًا مُثبتًا عن مفترض.

11. المراجع

  • سجل CVE - https://www.cve.org/CVERecord?id=CVE-2026-19264
  • GitHub Security Advisory - https://github.com/gitroomhq/postiz-app/security/advisories/GHSA-4hgh-5rhf-4qpm
  • استشارة Postiz CNA (PSA-2026-TH12B7) - https://gadvisory.org/advisories/PSA-2026-TH12B7
  • الالتزام بالإصلاح - https://github.com/gitroomhq/postiz-app/commit/7936062
  • الإصدار المرمم v2.22.1 - https://github.com/gitroomhq/postiz-app/releases/tag/v2.22.1
  • CWE-22 - https://cwe.mitre.org/data/definitions/22.html

إرشادات المعالجة

إذا كنت تستضيف Postiz ذاتيًا:

  1. رقّ إلى v2.22.1 أو أحدث. هذا هو الإصلاح.
  2. افترض أن JWT_SECRET مخترق إذا شغّلت إصدارًا متأثرًا على مضيف متاح عمومًا مع STORAGE_PROVIDER=local. دَوّره. ولأن الرموز لا تحمل انتهاء صلاحية، فإن التدوير هو الطريقة الوحيدة لإبطال أي رمز مُزوّر.
  3. دَوّر بيانات اعتماد DATABASE_URL وأي أسرار OAuth للمزود المتصل المخزنة في نفس البيئة.
  4. افحص سجلات الوصول بحثًا عن طلبات GET إلى /uploads/ تحتوي على %2e أو %2f.

بحث أُجري باستقلالية وأُفصح عنه للبائع ضمن إفصاح منسق. نُشر بعد إصدار الإصلاح ونشر الاستشارة. لم تُصل إلى أي أنظمة طرف ثالث - كل التحقق تم ضد مثيل محلي مبني من مصدر المشروع نفسه.


الترخيص

هذا التقرير مرخّص بموجب CC BY 4.0 - شاركه وعدّله بحرية مع الإسناد. مقتطفات الكود من gitroomhq/postiz-app مقتبسة للتحليل الأمني وتبقى خاضعة لترخيص ذلك المشروع.

Krithik Babu P - @DarkLycn1976

تنزيل الأداة
12:18تم الالتزام بالإصلاح والتحقق منه ونشره
2026-08-07 14:13CVE-2026-19264 معيّنة من قبل Postiz (CNA)
2026-08-07 14:15نشر GitHub Security Advisory