
CVE-2026-19264 - ثغرة حرجة لتجاوز المسار دون مصادقة تؤدي إلى السيطرة الكاملة على المثيل في Postiz (< 2.22.1). توثيق تقني: تجاوز ترتيب فك التشفير، تصعيد JWT_SECRET، وتحليل الإصلاح من المصدر العلوي.

المؤلف: 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 واحد غير مصادَق يؤدي إلى السيطرة الكاملة على المثيل.
Postiz هو منصة جدولة وسائل تواصل اجتماعي مفتوحة المصدر - نحو 34,000 نجمة على GitHub وقت كتابة هذا التقرير - مبنيّة بواجهة أمامية Next.js وخلفية NestJS. تستضيفه ذاتيًا وكالات وفرق صغيرة على نطاق واسع لإدارة حسابات التواصل الاجتماعي المتصلة والمحتوى المجدول والفواتير.
يمكن للنشر الذاتي تخزين الوسائط المرفوعة محليًا بدلًا من التخزين الكائني. يتحكم في هذا السلوك متغير بيئة واحد:
STORAGE_PROVIDER=local
هذه هي القيمة المضمّنة في .env.example، لذا فهي ما يشغّله معظم المستضيفين ذاتيًا ما لم يكوّنوا S3 أو Cloudflare R2 عمدًا.
عندما يكون التخزين المحلي مفعّلًا، يعيد next.config.js كتابة المسار العام /uploads/:path* إلى مسار API داخلي:
apps/frontend/src/app/(app)/api/uploads/[[...path]]/route.ts
خاصيتان تجعلان هذا المسار مثيرًا للاهتمام قبل أي ثغرة:
[[...path]] يعني أن كل مقطع مسار متبقٍ يصل كمصفوفة يمكن للمعالج تفسيرها بحرية.عندما يكون STORAGE_PROVIDER أي قيمة غير local، تشير إعادة الكتابة إلى /404 ويصبح المعالج غير قابل للوصول. بوابة التكوين هذه هي الشيء الوحيد الذي يفصل بين النشر وهذه الثغرة.
المعالج، قبل الإصدار v2.22.1:
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 يُستنتج من اسم الملف. لا توجد قائمة سماح بالامتدادات ولا مرشح محتوى.
الهجوم النموذجي هو:
GET /uploads/../../../etc/passwd
يُرجع هذا 404 على Postiz، وهذا الـ 404 هو السبب الكامل وراء بقاء هذه الثغرة حتى اكتُشفت.
يطبّع Next.js مسار الطلب أثناء التوجيه. تُطوى مقاطع ../ الخام قبل أن يقرر الموجّه أي معالج يستدعي. بحلول وصول الطلب إلى مسار الالتقاط، يكون الاجتياز قد أُزيل بالفعل - إما أن المسار يُحل إلى مكان لا يوجد فيه مسار مطابق، أو يُحل إلى داخل /uploads مع اختفاء المقاطع المنقوطة.
بالنسبة لمن يختبر بسرعة، يقرأ ذلك الـ 404 كأنه "الإطار يعالج هذا". إنه دفاع حقيقي وفعّال. المشكلة ليست في غيابه - بل في مكان تنفيذه في خط الأنابيب.
لا تؤدي مطابقة المسار ومعالج الطلب نفس عدد مرات فك ترميز النسبة المئوية.
إذا كانت الفواصل مشفّرة بالنسبة المئوية، فإن التسلسل ليس فاصل مسار أثناء مطابقة المسار. %2e%2e%2f مجرد سلسلة معتمة - نص خامل ليس لدى المطبّع سبب للمسه. يمر عبر التوجيه سليمًا، يطابقه مسار الالتقاط، ويُفك ترميزه في طريقه إلى params الخاصة بالمعالج، حيث يصبح ../ من جديد.
عند تلك النقطة يُضم إلى UPLOAD_DIRECTORY ويُسلَّم إلى createReadStream() - متجاوزًا التوجيه والتطبيع وكل ضابط كان سيمنعه.
أشكال مجدية:
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).
بدائية قراءة الملفات هي ثغرة عالية بحد ذاتها. ما يجعلها حرجة هو ما تصل إليه.
الخطوة 1 - قراءة البيئة. إعدادات عملية Node نفسها موجودة على القرص في جذر النشر. يوفّر .env، من بين أمور أخرى:
JWT_SECRET - مفتاح توقيع رموز الجلسةDATABASE_URL - بيانات اعتماد Postgres كاملةالخطوة 2 - تزوير جلسة. يوقّع Postiz رموز الجلسة بـ JWT_SECRET باستخدام HS256 عبر jsonwebtoken. والأهم أن الرموز تُصدر بدون expiresIn، لذا يكون الرمز المزوّر صالحًا إلى أجل غير مسمى.
الخطوة 3 - كن أي شخص. يعيد برمجية المصادقة الوسيطة حل المستخدم من قاعدة البيانات باستخدام مطالبة id. وهي عمدًا لا تثق بمطالبة مثل isSuperAdmin من الرمز - تصميم جيد - لكن هذا التحصين غير ذي صلة بمجرد أن تتمكن من توقيع id عشوائي. توقيع { id: <معرف المستخدم الضحية> } ينتج جلسة لا يمكن تمييزها عن تسجيل دخول مشروع:
read .env → JWT_SECRET → sign({ id: victim }) → authenticated as victim, forever
تحققت من هذا مقابل منطق التحقق الحقيقي للمشروع مع اعتماد jsonwebtoken الفعلي: قُبل رمز موقّع بالسر المسترجع، ورُفض رمز موقّع بسر خاطئ. حالة التحكم مهمة - بدونها لديك افتراض، لا اكتشاف.
الخطوة 4 - المسار الموازي. DATABASE_URL وحده كافٍ للوصول المباشر إلى Postgres: اقرأ كل حساب متصل، أو اقلب علامة المسؤول مباشرة.
لا كلمة مرور. لا وصول سابق. لا تفاعل مستخدم. طلب HTTP واحد غير مصادَق.
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 هي الافتراضي المُسلَّم.
تصحيح المطوّرين (7936062) ثمانية أسطر ويستحق القراءة، لأنه صحيح بطريقة لا تكون عليها هذه الإصلاحات غالبًا: