📚 محتوى دفاعي بحت — يفترض هذا الدرس معرفة أساسية بالمصادقة (الدرس السابع) قبل التعمّق بأخطاء JWT الشائعة.
تذكير سريع: بنية JWT
رمز JWT ثلاثة أجزاء مفصولة بنقطة، كل جزء مُرمَّز Base64 (وليس مُشفَّرًا):
header.payload.signature
- header: يحدّد خوارزمية التوقيع (
alg)، مثلHS256أوRS256. - payload: الادّعاءات (claims) — هوية المستخدم، الصلاحيات، وقت الانتهاء
exp. - signature: توقيع رياضي على الجزأين الأولين، يضمن أنهما لم يُعدَّلا.
⚠️ الـ payload مقروء لأي شخص لأنه Base64 فقط لا تشفير — لا تضع بيانات حسّاسة فيه (كلمات مرور، أرقام بطاقات). التوقيع يمنع التعديل، لا يمنع القراءة.
الخطأ الأول: قبول alg: none
مواصفة JWT تسمح بخوارزمية اسمها none — أي "بلا توقيع إطلاقًا". لو قبل الخادم رمزًا بهذه الخوارزمية، يستطيع أي شخص تعديل الـ payload (مثلًا تغيير الدور لـ admin) دون أي توقيع صالح، لأن الخادم لا يتحقّق من توقيع لا وجود له.
// ❌ خطر: يثق بخوارزمية الرمز نفسه بدل فرض خوارزمية محدّدة
const payload = jwt.verify(token, secretOrPublicKey);
الدفاع: لا تدع مكتبة JWT تقرّر الخوارزمية من الرمز نفسه — افرض خوارزمية واحدة صراحة عند التحقّق، وارفض أي رمز غيرها (بما فيها none).
// ✅ آمن: خوارزمية محدّدة صراحة، لا تعتمد على ما يدّعيه الرمز نفسه
const payload = jwt.verify(token, secret, { algorithms: ["HS256"] });
الخطأ الثاني: الخلط بين decode و verify
بعض المكتبات توفّر دالة decode() تقرأ محتوى الرمز دون التحقّق من التوقيع، ودالة verify() تتحقّق فعليًّا. استخدام decode() عن طريق الخطأ بمسار مصادقة يعني قبول أي رمز مزوَّر بالكامل.
⚠️ استخدم
verify()دائمًا في أي مسار يعتمد عليه قرار أمني —decode()مفيد فقط لقراءة معلومات غير حسّاسة من رمز موثوق مسبقًا.
الخطأ الثالث: سرّ ضعيف أو افتراضي
خوارزميات التوقيع المتماثل (مثل HS256) تعتمد على سرّ (secret) واحد للتوقيع والتحقّق معًا. سرّ قصير أو منسوخ من مثال توثيق يمكن تخمينه بهجوم قاموس بلا حاجة لاختراق الخادم إطلاقًا.
- استخدم سرًّا عشوائيًّا طويلاً (32 بايت فأكثر) خاصًّا بتطبيقك.
- فضّل خوارزميات المفتاح العام/الخاص (
RS256/ES256) عند الحاجة لتوزيع التحقّق على أطراف متعدّدة — المفتاح العام لا يكفي وحده للتوقيع، فتقليل ضرر تسريبه أكبر.
الخطأ الرابع: عدم التحقّق من انتهاء الصلاحية والجمهور
exp(انتهاء الصلاحية): رمز وصول (access token) طويل الأمد يبقى صالحًا حتى لو سُرِّب. اجعل مدّته قصيرة (دقائق) واستخدم رمز تجديد (refresh token) منفصل أطول أمدًا لإصدار رموز جديدة.aud(الجمهور المقصود): بدون التحقّق منه، رمز صُدر لخدمة معيّنة قد يُقبل بالخطأ في خدمة أخرى تثق بنفس الجهة المُصدِرة.
أين تُخزَّن الرموز في المتصفّح؟
| المكان | الخطر |
|---|---|
localStorage | مقروء بالكامل من أي كود JavaScript يعمل بالصفحة — عرضة للسرقة عبر XSS |
HttpOnly cookie مع Secure و SameSite | غير مقروء من JavaScript، ويحتاج حماية CSRF إضافية (الدرس السابق عن CSRF) |
💡 لا يوجد مكان "آمن مطلقًا" — لكن
HttpOnlycookie يزيل خطر XSS الكامل مقابل الحاجة لحماية CSRF بجانبه، وهذا عادة مقايضة أفضل من تعريض الرمز كاملًا لأي سكربت بالصفحة.
قائمة تحقّق سريعة
- افرض خوارزمية توقيع محدّدة عند التحقّق، وارفض
noneصراحة. - استخدم
verify()لاdecode()في أي قرار مصادقة. - سرّ عشوائي طويل خاصّ بالتطبيق، لا افتراضي أو منسوخ من مثال.
- مدّة صلاحية قصيرة لرمز الوصول + رمز تجديد قابل للإلغاء.
- لا تضع بيانات حسّاسة في الـ payload — أي شخص يقرأه.
🎯 التالي: أمن سلسلة التوريد البرمجية.