ترميز المخرجات وسياسة 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 والترميز:
الخلاصة
- الترميز يتبع السياق؛ وترميز
HTMLالشامل للاقتباس يخدم النص والسمة معًا. - لا تضع مدخل المستخدم داخل وسم
scriptولا في روابطjavascript:. CSPيخبر المتصفح بمصادر السكربتات الموثوقة فيوقف ما تسلّل.- الترميز يمنع الحقن، و
CSPيحدّ أثره؛ كلاهما لا أحدهما.
تحدٍّ برمجي: ترميز في سياقين
متوسطهذا الخادم يضع اسم المستخدم في موضعين دون ترميز: قيمة سمة data-name، ونص العنوان. أمّن الموضعين بترميز يشمل علامة الاقتباس.
- رمّز الاسم قبل وضعه.
- الاسم العادي يظهر كما هو.
- محاولة كسر السمة بعلامة اقتباس يجب أن تُرمَّز (لا تظهر
"خام تفتح سمة جديدة).
👀 اعرض الاستجابات المطلوبة
GET /greet?name=نورة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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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);