طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 243 · أمن الويب
الدرس 8 · الوحدة 4: تنفيذ ثغرات معروفة

XSS وحقن الأوامر في المختبر

كيف يصبح تعليق بريء سكربتًا يسرق جلسات الزوار، وكيف نمنعه؟ ⏱ نحو 50 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح كيف يتحوّل مدخل المستخدم إلى سكربت في XSS.
  • تميّز بين XSS المخزَّن والمنعكس وفي DOM.
  • تصلح XSS بترميز المخرجات، وتدعمه بـ CSP.
  • تتجنّب حقن أوامر النظام بتمرير الوسائط منفصلة.

١XSS: حين يصبح تعليق المستخدم كودًا

تطبيق الجذر نفسه على صفحة HTML بدل قاعدة البيانات يعطي XSS (البرمجة عبر المواقع): مدخل المستخدم يُدمج في الصفحة فيصبح HTML — وربما script — يُنفَّذ في متصفح زائر آخر.

هذا خادم تعليقات يخزّن ما يُرسل ثم يعرضه مدموجًا في HTML. تعليق عادي يُعرض نصًّا:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); let comments = []; // ✗ يعرض التعليقات بدمجها في HTML مباشرةapp.post('/comment', (req, res) => { comments.push(req.body.text || ''); res.json({ ok: true }); });app.get('/comments', (req, res) => {  const html = '<ul>' + comments.map(c => '<li>' + c + '</li>').join('') + '</ul>';  res.type('html').send(html);}); app.listen(3000);

التعليق البريء ظهر كما هو داخل عنصر القائمة.

لكن إن احتوى التعليق وسمًا، يُدمج في الصفحة فيصير جزءًا منها. هنا نضع وسمًا ظاهرًا للتوضيح؛ ولو كان <script> لنُفِّذ في متصفح كل من يفتح صفحة التعليقات:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); let comments = []; // ✗ يعرض التعليقات بدمجها في HTML مباشرةapp.post('/comment', (req, res) => { comments.push(req.body.text || ''); res.json({ ok: true }); });app.get('/comments', (req, res) => {  const html = '<ul>' + comments.map(c => '<li>' + c + '</li>').join('') + '</ul>';  res.type('html').send(html);}); app.listen(3000);

الوسم صار فعّالًا في الصفحة. هذا XSS مخزَّن: الحمولة تُحفظ في الخادم وتضرب كل زائر لاحق. ولو كان script لسرق جلسات الزوار أو غيّر الصفحة أمامهم.

٢أنواعه ولماذا هو خطير

النوعأين تُحفظ الحمولةالضحية
مخزَّن Storedفي الخادم (تعليق، اسم ملف)كل من يفتح الصفحة
منعكس Reflectedفي الرابط، تنعكس في الردمن يُفتَح له الرابط
في DOMيبنيه جافاسكربت في المتصفحالمستخدم الحالي

خطره أن الكود يعمل داخل جلسة الضحية وصلاحياتها: يقرأ رمز الجلسة فيسرق الحساب، أو ينفّذ أفعالًا باسم الضحية، أو يزرع صفحة دخول مزيفة. السرية والسلامة معًا في خطر.

٣الإصلاح: ترميز المخرجات

الدفاع الجذري هو الترميز عند الإخراج (الدرس 4): قبل دمج أي قيمة في HTML، حوّل محارفها الخاصة إلى صورتها الحرفية، فتُعرض نصًّا لا وسمًا:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); let comments = [];function esc(s) {  return String(s).replace(/[&<>"']/g, c =>    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));}app.post('/comment', (req, res) => { comments.push(req.body.text || ''); res.json({ ok: true }); });app.get('/comments', (req, res) => {  const html = '<ul>' + comments.map(c => '<li>' + esc(c) + '</li>').join('') + '</ul>';  res.type('html').send(html);}); app.listen(3000);

الآن يظهر النص «اقرأ هذا» حرفيًّا؛ رُمِّزت < و > فلم يعُد وسمًا. وفي أطر الواجهة مثل React يتم هذا الترميز تلقائيًّا، ما لم تستخدم dangerouslySetInnerHTML.

٤حقن أوامر النظام

النمط نفسه مع أوامر نظام التشغيل: إن بنيت أمر صدفة بدمج مدخل المستخدم، قد يضيف المهاجم أمرًا آخر بفاصل مثل ; أو &&. القاعدة:

الخطرالأأمن
exec('ping ' + req.query.host)تجنّب الصدفة أصلًا
بناء أمر نصيّexecFile('ping', ['-c', '1', host]) (وسائط منفصلة)
الثقة بالمدخلقائمة مسموح للقيم المتوقعة

القاعدة العامة: لا تبنِ أمرًا أو استعلامًا أو صفحة بدمج نصيّ. مرّر القيم منفصلة (وسائط، معاملات)، أو رمّزها للسياق.

توقّع وتحقّق

XSS المخزَّن يضرب:

الدفاع الجذري عن XSS:

لتجنّب حقن الأوامر نفضّل:

رأس CSP دوره:

الخلاصة

تحدٍّ برمجي: سدّ ثغرة XSS

متقدم

خادم الملاحظات يخزّن ما يرسله المستخدم ويعرضه مدموجًا في HTML، فهو عرضة لـ XSS المخزَّن. أصلحه بترميز المخرجات دون تغيير سلوكه الطبيعي.

المطلوب:
  • رمّز كل ملاحظة قبل دمجها في HTML.
  • يظل حفظ الملاحظة وعرضها يعملان.
  • ملاحظة فيها <b> يجب أن تظهر نصًّا حرفيًّا لا وسمًا فعّالًا.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /notes {"text": "ملاحظة عادية"}
  2. POST /notes {"text": "<b>خطير</b>"}
  3. GET /notes
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • حفظ الملاحظة ← 201
  • العدّاد يزيد
  • عرض الملاحظات ← 200
  • الملاحظة العادية تظهر
  • الوسم الخطر مُرمَّز (يظهر <b>)
  • لا يظهر وسم b فعّال

💡 تلميح 1
انسخ دالة esc من مثال الدرس التي ترمّز < > & " '.
💡 تلميح 2
طبّقها على كل ملاحظة: notes.map(n => '
  • ' + esc(n) + '
  • ')
    .
    💡 تلميح 3
    الترميز عند الإخراج (العرض) لا عند الإدخال.

    حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

    const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); let notes = [];function esc(s) {  return String(s).replace(/[&<>"']/g, c =>    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));}app.post('/notes', (req, res) => { notes.push(req.body.text || ''); res.status(201).json({ count: notes.length }); });app.get('/notes', (req, res) => {  const html = '<h2>الملاحظات</h2><ul>' + notes.map(n => '<li>' + esc(n) + '</li>').join('') + '</ul>';  res.type('html').send(html);});app.listen(3000);
    انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

    📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس