طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 243 · أمن الويب
الدرس 10 · الوحدة 5: تصميم تطبيقات مقاومة للهجمات

ترميز المخرجات وسياسة CSP

الترميز يختلف باختلاف الموضع، و CSP شبكة أمان حين يفلت شيء. ⏱ نحو 45 دقيقة.

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

١الترميز يتبع السياق

ترميز HTML الذي تعلّمناه يحمي النص داخل العناصر. لكن المدخل قد يُوضع في مواضع أخرى، ولكلٍّ محارفه الخطرة:

السياقالمحارف الخطرةالترميز
نص داخل عنصر <h3>هنا</h3>< > &ترميز HTML
قيمة سمة title="هنا"" ' < >ترميز HTML للسمة (مع علامات اقتباس)
داخل سكربت <script>هنا</script>كل شيء تقريبًاتجنّبه؛ أو ترميز JSON
في رابط href="هنا"javascript:تحقق من المخطط + ترميز URL

انظر مثالًا يضع الاسم في موضعين (نص وسمة)، ويرمّزه لكليهما فيبقى آمنًا:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); function escHtml(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));} app.get('/card', (req, res) => {  const name = req.query.name || 'زائر';  // الاسم يظهر في موضعين: نص داخل عنصر، وقيمة سمة title  const safe = escHtml(name);  res.type('html').send('<div title="' + safe + '"><h3>' + safe + '</h3></div>');}); app.listen(3000);

حاول الطلب الثاني كسرَ السمة بعلامة اقتباس لحقن حدث؛ لكن الاقتباس رُمِّز (") فبقي داخل السمة نصًّا. الترميز نفسه يخدم السياقين لأنه يشمل علامة الاقتباس.

٢قاعدة ذهبية: البيانات لا تصير كودًا

أسهل طريق للأمان أن تمنع المدخل من دخول سياق تنفيذيّ أصلًا:

٣الطبقة الثانية: Content-Security-Policy

الترميز يمنع الحقن، وCSP يحدّ أثره لو تسلّل شيء: رأس يخبر المتصفح من أين يُسمح بتحميل السكربتات والموارد. مع سياسة صارمة، حتى سكربت محقون لن يُنفَّذ لأنه ليس من مصدر موثوق:

const express = require('express');const helmet = require('helmet');const app = express(); app.use(helmet.contentSecurityPolicy({  directives: {    defaultSrc: ["'self'"],            // كل شيء من موقعنا فقط    scriptSrc: ["'self'"],             // لا سكربتات مضمّنة ولا خارجية    objectSrc: ["'none'"],  },})); app.get('/', (req, res) => res.type('html').send('<h1>متجر رفيق</h1>'));app.listen(3000);

رأس Content-Security-Policy يحصر المصادر في موقعنا (self) ويمنع السكربتات المضمّنة. لو حُقن وسم script في الصفحة، رفض المتصفح تنفيذه لمخالفته السياسة.

التوجيهالمعنى
default-src 'self'الموارد من نطاقنا فقط افتراضيًّا
script-src 'self'السكربتات من نطاقنا فقط، لا مضمّنة
object-src 'none'لا إضافات (Flash/plugins)
frame-ancestors 'none'يمنع تضمين الموقع في إطار

CSP دفاع بالعمق لا بديل عن الترميز: الترميز يمنع الحقن أصلًا، و CSP شبكة أمان إن أخطأ الترميز في موضع.

توقّع وتحقّق

الترميز الصحيح يتبع:

أين لا يجوز وضع مدخل المستخدم؟

رأس CSP يحدّ أثر XSS بـ:

CSP والترميز:

الخلاصة

تحدٍّ برمجي: ترميز في سياقين

متوسط

هذا الخادم يضع اسم المستخدم في موضعين دون ترميز: قيمة سمة data-name، ونص العنوان. أمّن الموضعين بترميز يشمل علامة الاقتباس.

المطلوب:
  • رمّز الاسم قبل وضعه.
  • الاسم العادي يظهر كما هو.
  • محاولة كسر السمة بعلامة اقتباس يجب أن تُرمَّز (لا تظهر " خام تفتح سمة جديدة).
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /greet?name=نورة
  2. GET /greet?name="><script>
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • الترحيب يعمل ← 200
  • الاسم العادي يظهر
  • الاقتباس والوسم مُرمَّزان
  • لا يظهر وسم script فعّال
  • يستخدم دالة ترميز

💡 تلميح 1
انسخ escHtml التي ترمّز < > & " '.
💡 تلميح 2
رمّز الاسم مرة واحدة ثم ضعه في الموضعين.
💡 تلميح 3
ترميز علامة الاقتباس " هو ما يمنع كسر السمة.

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

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); function escHtml(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}app.get('/greet', (req, res) => {  const name = escHtml(req.query.name || 'زائر');  res.type('html').send('<div data-name="' + name + '"><h2>مرحبًا ' + name + '</h2></div>');});app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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