ما هي البرمجة العابرة للمواقع
البرمجة العابرة للمواقع (XSS) ثغرة يجعل فيها مهاجم صفحة في موقعك تضمّن JavaScript كتبه هو. لا يستطيع المتصفح تمييز ذلك السكربت عن سكربتك الخاص: يصل من أصل موقعك (origin)، فيعمل بكل ما يُسمح لأصلك بفعله، بما فيها جلسات مستخدميك.
السبب الجذري دائمًا نفسه. نص يتحكم فيه شخص آخر، كتعليق، أو مصطلح بحث، أو اسم عرض، أو مقطع من رابط، يُدرَج في صفحة بطريقة تتيح للمتصفح قراءته كترميز أو كود بدل نص عادي. تعليق ينبغي أن يظهر كحروف حرفية <script> يصبح بدلًا من ذلك عنصر سكربت.
الاسم تاريخي ومُضلِّل قليلًا: شمل الهجوم الكلاسيكي موقعًا ثانيًا، لكن معظم XSS اليوم هو ببساطة بيانات غير موثوقة تُنفَّذ داخل صفحتك الخاصة. تضع OWASP ذلك تحت فئة الحقن في OWASP Top 10، بجانب نظيره من جهة الخادم، حقن SQL. الفارق هو المُفسِّر. حقن SQL يخدع قاعدة بياناتك؛ وXSS يخدع متصفحات مستخدميك.
XSS خطأ في التطبيق، لا في المتصفح أو الشبكة. لا يمنعه HTTPS، ولا يمنعه جدار حماية بشكل كامل، ولا يستطيع إزالته إلا الكود الذي يبني الصفحة.
الأنواع الثلاثة: مخزَّن، ومنعكس، وقائم على DOM
XSS المخزَّن (يُسمّى أيضًا الدائم) هو الأكثر ضررًا. يُحفظ الإدخال الخبيث من الخادم، في تعليق، أو حقل ملف شخصي، أو مراجعة منتج، أو تذكرة دعم، ثم يُقدَّم لكل زائر يفتح تلك الصفحة، وغالبًا بما فيهم المسؤولون الذين يقرؤون المكتب الخلفي. لا يحتاج أحد النقر على رابط خاص.
XSS المنعكس غير مخزَّن. يأخذ الخادم شيئًا من الطلب، عادةً معامل استعلام، ويكتبه مباشرةً في الاستجابة: صفحة بحث تطبع "نتائج لـ..."، أو صفحة خطأ ترجّع القيمة الفاسدة. يجب على المهاجم أن يجعل الضحية تفتح رابطًا مُعدًّا، عبر بريد إلكتروني أو محادثة أو موقع آخر.
XSS القائم على DOM يحدث كاملًا في المتصفح. يقرأ كود JavaScript الخاص بك قيمة يتحكم فيها المهاجم، مثل location.hash أو location.search أو بيانات postMessage أو document.referrer، ويكتبها في بالوعة (sink) خطرة: innerHTML، أو document.write، أو eval، أو setTimeout بنص، أو رابط javascript: في href. قد لا يرى الخادم الحمولة أصلًا، لأن كل ما بعد # لا يُرسَل مع الطلب.
تُظهر الأمثلة الثلاثة المدرسية أدناه شكل كل خطأ. الحل في كل حالة الفكرة نفسها: عامِل القيمة كنص.
أنماط عرضة للاختراق بأدنى صورها (لا تنشرها)
<!-- Stored: a saved comment printed without escaping (PHP) -->
<p><?= $comment->body ?></p>
<!-- body saved as: <script>alert(document.domain)</script> -->
<!-- Reflected: the search term echoed from the URL -->
<h1>Results for <?= $_GET['q'] ?></h1>
<!-- link: /search?q=<script>alert(document.domain)</script> -->
// DOM-based: client code writes the URL fragment as HTML
document.getElementById('greeting').innerHTML =
decodeURIComponent(location.hash.slice(1));
// link: /welcome#<img src=x onerror=alert(document.domain)>
ما يستطيع المهاجم فعله به
alert(1) هو كيف يُثبت المختبرون الثغرة؛ ليس ما يُشغّله المهاجمون. حالما يُنفَّذ سكربتهم في صفحتك، يعمل كالمستخدم المسجّل دخوله، داخل أصلك (origin).
سرقة الجلسة. إن كان ملف تعريف ارتباط الجلسة قابلًا للقراءة من JavaScript، يرسله السكربت إلى المهاجم، الذي يسجّل دخوله بعدها كالضحية. الرموز المحفوظة في localStorage أو sessionStorage قابلة للقراءة من السكربت دومًا، وهذا هو الحجة الرئيسية ضد تخزين رموز طويلة العمر هناك.
أفعال بصفة المستخدم. حتى إن كان ملف تعريف الارتباط HttpOnly، يستطيع السكربت استدعاء واجهتك البرمجية بـfetch؛ ويُرفق المتصفح ملف تعريف الارتباط من نفسه. يستطيع قراءة رموز CSRF من الصفحة، وتغيير عنوان البريد الإلكتروني للحساب، وإنشاء مستخدم مسؤول، ونشر رسائل، أو تقديم طلبات. لا توقف قواعد الأصل نفسه، ولا CORS، ولا ملفات تعريف الارتباط SameSite هذا، لأن الطلب يأتي من موقعك أنت.
قراءة ما يراه المستخدم. البيانات الشخصية، والرسائل، والفواتير، وأي شيء على الصفحة أو يمكن الوصول إليه عبر واجهتك البرمجية.
التصيد وتسجيل ضربات المفاتيح على نطاقك. يستطيع السكربت إعادة رسم الصفحة كنموذج تسجيل دخول أو تسجيل ما يكتبه المستخدم في نماذج حقيقية. يُظهر شريط العنوان نطاقك وشهادة صالحة، فلا سبب للمستخدم ليشك فيها.
التشويه والانتشار. يستطيع XSS المخزَّن تغيير ما يراه كل زائر، أو نسخ نفسه إلى الملف الشخصي لكل ضحية. انتشرت دودة Samy عام 2005 على MySpace إلى أكثر من مليون ملف شخصي في أقل من يوم بهذه الطريقة.
مدى سوء ثغرة XSS معيّنة يتوقف على من يرى الصفحة. خطأ في شاشة مخصصة للمسؤولين فقط ويُصَل إليه بإدخال مخزَّن، كعنوان تذكرة دعم، غالبًا أسوأ من خطأ في الصفحة الرئيسية العامة.
الحل: ترميز الإخراج الواعي بالسياق
الدفاع الأساسي هو ترميز البيانات غير الموثوقة في لحظة كتابتها في الصفحة، بالطريقة التي يتطلبها السياق المحيط. التحقق من الإدخال يساعد (الرمز البريدي ينبغي أن يبدو كرمز بريدي)، لكنه لا يمكن أن يكون الضابط الرئيسي، لأن القيمة نفسها قد تكون آمنة في سياق وخطِرة في آخر.
جسم HTML: رمّز & و< و> و" و' إلى كيانات، فيُعرَض <script> بدل أن يُحلَّل.
سمة HTML: ضع قيم السمات دائمًا بين علامتي تنصيص، ورمّز الأحرف نفسها. يمكن الخروج من سمة غير محاطة بعلامات تنصيص بمسافة واحدة.
رابط في href أو src: الترميز غير كافٍ، لأن javascript:alert(1) لا يحتوي شيئًا لتهريبه. حلّل الرابط واسمح فقط بـhttps: وَhttp: (وَmailto: إن احتجت)؛ ورمّز قيم الاستعلام بـencodeURIComponent.
داخل كتلة <script>: لا تُجمِّع نصوصًا داخل JavaScript. سلسِل البيانات كـJSON بدالة تُهرِّب أيضًا <، أو ضعها في سمة data- واقرأها بـelement.dataset.
الأنماط (styles): تجنّب وضع بيانات المستخدم في CSS إطلاقًا.
على العميل: فضّل الواجهات الآمنة. لا تُحلِّل textContent وsetAttribute على سمات غير مؤذية وcreateElement HTML أبدًا؛ أما innerHTML وَouterHTML وَinsertAdjacentHTML وَdocument.write فتحلّله.
كتابة بيانات المستخدم بأمان في المتصفح
// Text, never markup
el.textContent = userName;
// Links: allow only http(s)
function safeUrl(value) {
try {
const url = new URL(value, location.origin);
return ['https:', 'http:'].includes(url.protocol) ? url.href : '#';
} catch {
return '#';
}
}
link.href = safeUrl(profile.website);
// Data for scripts: a data attribute, not string concatenation
// <div id="app" data-user="{{ user_json }}"> (escaped by the template)
const user = JSON.parse(document.getElementById('app').dataset.user);
الترميز الآلي في أطر العمل، ومنافذ تجاوزه
تُرمِّز أطر العمل الحديثة بالفعل لسياق HTML افتراضيًا. تُهرِّب {{ }} في Blade وTwig وJinja وقوالب Django وVue وAngular، و<%= %> في Rails ERB، و{value} في React JSX كل ما تطبعه. هذا السبب الرئيسي في أن XSS أصبح أندر مما كان في صفحات PHP المكتوبة يدويًا، وسبب للاستمرار في استخدام طريقة الطباعة الافتراضية لإطار العمل.
تعيش معظم ثغرات XSS الحقيقية في قاعدة كود حديثة في منفذ تجاوز (escape hatch)، ميزة تُعطّل الترميز عمدًا:
- React dangerouslySetInnerHTML، وVue v-html، وSvelte {@html}، وAngular bypassSecurityTrustHtml.
- Laravel Blade {!! $value !!}، وJinja |safe وَMarkup()، وDjango mark_safe وَ{% autoescape off %}، وRails raw وَhtml_safe.
- كتابات DOM مباشرة من كود المكوّن: ref.current.innerHTML = ...، وjQuery .html()، وَ$(userInput).
الثغرات الأخرى سياقات لا يفهمها المُرمِّز الآلي. قالب يُرمِّز HTML لا يزال يسمح بـjavascript: في href={url}، ولا يزال يسمح لقيمة مستخدم داخل <script> مضمَّن أو معالج حدث بكسر الكود، ولا يحمي أبدًا نصًّا تمرره إلى eval.
قاعدة عملية: اجعل منافذ التجاوز نادرة وقابلة للبحث ومراجَعة. يجد بحث في الكود عن القائمة أعلاه معظم تعرّضك، وتمنع قاعدة linter (مثل قاعدة ESLint react/no-danger، أو قاعدة Semgrep لـ{!!) تسلل منافذ جديدة بلا رصد.
حين يجب أن تقبل HTML: استخدم مُنقِّيًا حقيقيًا
تحتاج بعض الميزات HTML من المستخدم: محرر نصوص غني، أو جسم CMS، أو منشور منتدى بتنسيق، أو معاينة بريد إلكتروني. سيُدمِّر الترميز التنسيق، فالجواب هو التنقية: تحليل HTML والاحتفاظ فقط بقائمة سماح من العناصر والسمات الآمنة.
استخدم مكتبة مُصانة مبنية لهذا، لا تعبيرًا نمطيًا ولا قائمة سوداء لـ"الوسوم السيئة" أبدًا. تُحلِّل المتصفحات HTML بطرق مفاجئة، وكل مُرشِّح مكتوب يدويًا يحذف <script> تم تجاوزه بسمة معالج حدث، أو عنصر SVG، أو تداخل غريب، أو خدعة ترميز. الخيارات المعتمَدة هي DOMPurify في المتصفح وفي Node.js مع jsdom، وَHTML Purifier لـPHP، وَnh3 (مكتبة ammonia بـRust) لـPython، وَOWASP Java HTML Sanitizer لـJava.
ثلاثة تفاصيل تصنع الفارق. نقِّ بقائمة سماح صغيرة بقدر ما تحتاجه الميزة. نقِّ عند الإخراج، أو مرة أخرى عند الإخراج، كي يحمي تغيير لاحق في المكتبة أو في قائمة سماحك بيانات قديمة. ولا تعدّل HTML بعد تنقيته: إعادة التحليل، أو استبدال النصوص، أو إدراجه في سياق مختلف قد يحوّل نتيجة آمنة إلى غير آمنة.
لـMarkdown، يحتاج HTML المُصدَّر المعالجة نفسها: تسمح معظم مُصدِّرات Markdown بـHTML خام افتراضيًا.
الحد من الضرر: HttpOnly، وSameSite، وContent-Security-Policy
افترض أن ثغرة XSS واحدة ستتسرب في النهاية، واجعلها أقل قيمة.
ملفات تعريف الارتباط. علّم ملفات تعريف ارتباط الجلسة بـHttpOnly، كي لا يستطيع document.cookie قراءتها، بالإضافة إلى Secure وَSameSite=Lax أو Strict. يوقف ذلك سيناريو سرقة ملف تعريف الارتباط. لا يوقف السكربت من العمل بصفة المستخدم ما دامت الصفحة مفتوحة، فهو تحكم بالضرر لا حل. أبقِ الرموز طويلة العمر خارج localStorage لنفس السبب.
Content-Security-Policy. تخبر CSP المتصفح أي سكربتات يجوز تشغيلها. السياسة التي تُوقف XSS صارمة: nonce عشوائي يُولَّد جديدًا لكل استجابة، يوضع في الترويسة وعلى كل وسم <script> مشروع، بالإضافة إلى 'strict-dynamic' وَobject-src 'none' وَbase-uri 'none'. سكربت مُحقَن بلا nonce صالح، ومعالجات أحداث مضمَّنة مثل onerror= محجوبة، فتتحول معظم ثغرات XSS إلى خطأ في الكونسول وتقرير. سياسة تسرد النطاقات فقط وتحتفظ بـ'unsafe-inline' تمنح حماية ضعيفة ضد XSS.
ابدأ بها كـContent-Security-Policy-Report-Only أولًا، وأصلح ما تُظهره التقارير، ثم فعِّلها. يغطي دليل ترويسات أمان HTTP الطرح بوضع التقرير فقط، والترويسات الأخرى التي تنتمي بجانبها، ولماذا لا ينبغي إرسال X-XSS-Protection بعد الآن.
فخ تخزين مؤقت واحد: يجب أن يكون nonce غير قابل للتوقع. إن قدّم CDN أو ذاكرة تخزين مؤقت للصفحات نفس HTML للجميع، يحصل كل زائر على nonce نفسه، ويستطيع المهاجم قراءته من الصفحة. للصفحات المخزَّنة مؤقتًا، استخدم تجزئات السكربت ('sha256-...') بدلًا من ذلك، أو أبقِ HTML الذي يحمل nonce خارج التخزين المؤقت.
حيث تدعمه المتصفحات، تذهب require-trusted-types-for 'script' أبعد: ترفض بالوعات DOM مثل innerHTML النصوص العادية، فيجب أن يمر XSS القائم على DOM عبر كود كتبته أنت.
سياسة صارمة قائمة على nonce (nonce جديد لكل استجابة)
Content-Security-Policy: script-src 'nonce-R4nd0mPerResponse' 'strict-dynamic'; object-src 'none'; base-uri 'none'
<script nonce="R4nd0mPerResponse" src="/js/app.js"></script>
Set-Cookie: session=...; Path=/; Secure; HttpOnly; SameSite=Lax
اختبار تطبيقك بحثًا عن XSS
اختبر فقط أنظمة تملكها أو مُصرَّح لك باختبارها. لتطبيقك الخاص، تجد علامة غير مؤذية معظم الثغرات دون أي كود هجومي.
تتبّع كل إدخال إلى كل مخرج. ضع علامة فريدة تحتوي أحرفًا هامة في HTML، كمثال xss7"'<b>bold</b>، في كل حقل، ومعامل استعلام، وترويسة يعرضها تطبيقك، واسم ملف تقبله. ثم زُر كل صفحة تظهر فيها تلك القيمة، بما فيها شاشات المسؤولين، والرسائل، والتصديرات، والإشعارات. إن ظهرت الكلمة عريضة، أو أظهر مصدر الصفحة <b> بلا تهريب، أو علامة تنصيص تُغلق سمة، فالإخراج غير مُرمَّز.
فحص DOM، لا المصدر فقط. للثغرات القائمة على DOM، ضع العلامة في location.hash وسلسلة الاستعلام وافحص DOM المباشر في أدوات المطوّر: يُظهر view-source فقط ما أرسله الخادم.
بحث الكود. ابحث عن منافذ التجاوز المذكورة أعلاه وعن innerHTML وَinsertAdjacentHTML وَdocument.write وَeval وَnew Function. يجب أن يختفي كل نتيجة أو يحمل تعليقًا قصيرًا يشرح لماذا الإدخال آمن.
استخدم الأدوات. تزحف OWASP ZAP وBurp Suite وتختبران الإدخالات تلقائيًا وتجدان الحالات المنعكسة الواضحة؛ ويتبع التحليل الساكن مثل Semgrep أو CodeQL تدفقات بيانات لا يراها الزاحف. لا يستبدل أي منهما التتبع اليدوي لـXSS المخزَّن في شاشات المكتب الخلفي.
راقب تقارير CSP. حالما تُفعَّل سياسة بوضع التقرير فقط، تكون المخالفات من سكربتات مضمَّنة غير متوقعة إنذارًا مبكرًا مجانيًا.
grep -rnE 'dangerouslySetInnerHTML|v-html|\{@html|bypassSecurityTrust|innerHTML|insertAdjacentHTML|document\.write' src/
grep -rnE '\{!!|\|safe|mark_safe|html_safe|raw\(' resources/ templates/ app/
مكان WAF، وما يفعله WAF في CDN.com.tr
يفحص جدار حماية تطبيقات الويب الطلبات ويحجب ما يشبه الهجمات. لـXSS هذا مفيد: وسوم السكربت ومعالجات الأحداث في سلسلة استعلام أو جسم نموذج قابلة للتعرّف عليها، فتُوقَف المحاولات المنعكسة والفاحصات التلقائية قبل أن تصل إلى تطبيقك، وغالبًا تُرفَض حمولة مخزَّنة مُرسَلة عبر نموذج عادي في طريقها للدخول.
هو طبقة، لا الحل. يرى WAF الطلبات، لا صفحاتك، فلا يستطيع معرفة كيف ستُستخدم قيمة لاحقًا. لا يصل إليه XSS القائم على DOM الذي يعيش في مقطع الرابط أبدًا، ويتسرب إدخال يصل عبر استيراد، أو واجهة برمجية تستدعيها أنت، أو قناة لا يفحصها WAF، ويضبط المهاجمون الحمولات لتفادي الأنماط. يخرّط دليل OWASP Top 10 بالتفصيل ما يستطيع جدار حماية رصده وما لا يستطيع. رمّز الإخراج، ونقِّ HTML، واضبط CSP، سواء كان WAF أمامك أم لا.
على CDN.com.tr، الـWAF هو ModSecurity مع OWASP Core Rule Set، يعمل عند الحافة ويُفعَّل للحساب من صفحة قواعد التسليم. عائلة 941 في Core Rule Set هي قواعد البرمجة العابرة للمواقع. يحصل الزائر المحجوب على صفحة 403 بهوية العلامة التجارية مع رقم مرجعي، وتسرد صفحة سجلات WAF الأحداث المحجوبة بفئة الهجوم والبلد وIP والقاعدة، قابلة للبحث بذلك الرقم خلال آخر 30 يومًا وقابلة للتصدير إلى CSV أو XLSX؛ ويُرجع cdnctl waf logs نفس القائمة من سطر الأوامر.
الإيجابية الكاذبة المعتادة لقواعد XSS هي منشور HTML مشروع، كمحرر يحفظ محتوى منسَّقًا أو نموذج كود. الاستثناءات لمسارات مفردة غير متاحة في اللوحة بعد: إن حجب WAF طلبًا مشروعًا، أرسل رقمه المرجعي إلى الدعم بدل إيقاف WAF.
تستطيع إضافة الترويسات عند الحافة أيضًا. تدخل الترويسات أحادية القيمة في الرؤوس المخصصة لقاعدة تسليم، سطر واحد لكل ترويسة، وتُطبَّق على استجابات 2xx وَ3xx، بما فيها إصابات التخزين المؤقت. لا يمكن أن تذهب سياسة Content-Security-Policy كاملة إلى هناك، لأن اللوحة والحافة ترفضان ; في سطر ترويسة؛ توجيه واحد مثل frame-ancestors 'self' يعمل، ويجب توليد nonce لكل استجابة على أي حال، فالسياسة تنتمي إلى تطبيقك.
cdnctl waf logs --account $ACCOUNT_UUID --range 1d
cdnctl waf show $REFERENCE_ID --account $ACCOUNT_UUID
أسئلة شائعة حول XSS
ما الفرق بين XSS المخزَّن والمنعكس والقائم على DOM؟
أين تعيش الحمولة. يُحفظ XSS المخزَّن على الخادم ويُقدَّم لكل من يفتح الصفحة. يعود XSS المنعكس من الطلب نفسه، فيجب على الضحية فتح رابط مُعدّ. يُنشَأ XSS القائم على DOM من كود JavaScript الخاص بك من جانب العميل حين يكتب قيمة يتحكم فيها المهاجم، غالبًا من الرابط، في الصفحة؛ قد لا يراه الخادم أبدًا.
هل تحمي HTTPS أو شهادة صالحة من XSS؟
لا. يحمي TLS البيانات في النقل. تُسلَّم حمولة XSS من خادمك أنت أو سكربتك أنت عبر الاتصال المشفَّر نفسه، وتجعل القفل نموذج تسجيل دخول مزيفًا على نطاقك يبدو أكثر موثوقية، لا أقل.
هل HttpOnly كافٍ لإيقاف XSS؟
لا. يوقف السكربت من قراءة ملف تعريف ارتباط الجلسة، ما يُزيل نتيجة واحدة. لا يزال السكربت يستطيع إرسال طلبات بصفة المستخدم، لأن المتصفح يُرفق ملف تعريف الارتباط من نفسه، ولا يزال يستطيع قراءة الصفحة وتغييرها. HttpOnly تحكم بالضرر؛ ترميز الإخراج هو الحل.
هل يستطيع WAF إيقاف البرمجة العابرة للمواقع؟
يوقف كثيرًا من المحاولات المنعكسة والتلقائية، لأن حمولات السكربت في الطلبات قابلة للتعرّف عليها. لا يستطيع رؤية XSS القائم على DOM في مقطع الرابط، ولا يعرف كيف سيستخدم تطبيقك قيمة مخزَّنة، ويمكن تجاوزه بحمولة مُصمَّمة خصيصًا. عامله كطبقة أمام كود صحيح.
هل ينبغي أن أرسل X-XSS-Protection؟
لا. أُزيل المرشِّح الذي كانت تتحكم فيه من المتصفحات الحالية، وفي المتصفحات الأقدم كان يمكن إساءة استخدامه. أرسل شيئًا فارغًا أو X-XSS-Protection: 0، واستخدم Content-Security-Policy بدلًا منه.
هل تجعلني React أو Vue محصّنًا من XSS؟
تجعلان الحالة الشائعة آمنة بترميز ما تطبعه. تبقى معرّضًا عبر dangerouslySetInnerHTML وَv-html، وعبر روابط مستخدم في href تبدأ بـjavascript:، وعبر كتابات DOM مباشرة، وعبر HTML مُصدَّر من الخادم لا يتحكم فيه إطار العمل.
ما هو self-XSS؟
سكربت يُخدَع الضحية فيلصقه في كونسول متصفحه الخاص. لا يحتاج أي خطأ في موقعك، وهذا سبب تحذير المتصفحات عند اللصق في أدوات المطوّر. هو هندسة اجتماعية لا ثغرة في كودك.