يتطلب إنشاء تجربة غنية على الويب اليوم بشكل لا مفر منه تقريبًا تضمين مكونات ومحتوى لا يمكنك التحكم فيه بشكل حقيقي. يمكن أن تساهم التطبيقات المصغّرة التابعة لجهات خارجية في زيادة التفاعل وتؤدي دورًا مهمًا في تجربة المستخدم بشكل عام، كما أنّ المحتوى من إنشاء المستخدمين يكون أحيانًا أكثر أهمية من المحتوى الأصلي للموقع الإلكتروني. لا يمكن تجنّب أي منهما، ولكن كلاهما يزيد من خطر حدوث مشكلة في موقعك الإلكتروني. كل أداة تقوم بتضمينها - كل إعلان، كل أداة وسائل التواصل الاجتماعي - هي موجّه هجوم محتمل لأولئك الذين لديهم نوايا خبيثة:
يمكن أن تخفّف سياسة أمان المحتوى (CSP) من المخاطر المرتبطة بهذين النوعين من المحتوى من خلال منحك القدرة على إدراج مصادر موثوقة على وجه التحديد للنصوص البرمجية والمحتوى الآخر في القائمة المسموح بها. هذه خطوة مهمة في الاتجاه الصحيح، ولكن تجدر الإشارة إلى أنّ الحماية التي توفّرها معظم توجيهات CSP هي ثنائية: المورد مسموح به أو غير مسموح به. في بعض الأحيان، قد يكون من المفيد أن تقول "لست متأكدًا من أنّني أثق فعلاً بمصدر هذا المحتوى، ولكنّه رائع جدًا. يُرجى تضمينها، أيها المتصفح، ولكن لا تدعها تكسر موقعي".
الحدّ الأدنى من الأذونات المميّزة
في الأساس، نحن نبحث عن آلية تسمح لنا بمنح المحتوى الذي نضمّنه الحدّ الأدنى من الإمكانات اللازمة لأداء وظيفته. إذا لم تكن الأداة بحاجة إلى فتح نافذة منبثقة جديدة، لن يؤدي إيقاف إمكانية الوصول إلى window.open إلى حدوث أي مشكلة. إذا لم يكن يتطلب Flash، لن يكون إيقاف دعم المكوّنات الإضافية مشكلة. يمكننا تحقيق أعلى مستوى من الأمان إذا اتّبعنا مبدأ الحدّ الأدنى من الأذونات المميّزة وحظرنا كل ميزة لا ترتبط مباشرةً بالوظائف التي نريد استخدامها. ونتيجة لذلك، لم نعد مضطرين إلى الثقة بشكل أعمى في أن بعض أجزاء المحتوى المضمّن لن تستفيد من الامتيازات التي لا ينبغي استخدامها. ولن يتمكن ببساطة من الوصول إلى الوظيفة في المقام الأول.
وتُعدّ عناصر iframe الخطوة الأولى نحو إنشاء إطار عمل جيد لهذا الحلّ.
يوفّر تحميل بعض المكوّنات غير الموثوق بها في iframe قدرًا من الفصل
بين تطبيقك والمحتوى الذي تريد تحميله. لن يتمكّن المحتوى المُدرَج في الإطار من الوصول إلى نموذج العناصر في المستند (DOM) لصفحتك أو البيانات التي خزّنتها محليًا، ولن يتمكّن من الرسم في مواضع عشوائية على الصفحة، بل يقتصر نطاقه على مخطّط الإطار. ومع ذلك، فإنّ الفصل ليس قويًا حقًا. The contained page
still has a number of options for annoying or malicious behavior: autoplaying
video, plugins, and popups are the tip of the iceberg.
تمنحنا السمة sandbox للعنصر iframe ما نحتاجه بالضبط لتشديد القيود على المحتوى المؤطَّر. يمكننا
توجيه المتصفّح لتحميل محتوى إطار معيّن في بيئة
منخفضة الامتيازات، ما يسمح فقط بمجموعة فرعية من الإمكانات اللازمة لتنفيذ أي
عمل مطلوب.
ثق، ولكن تحقَّق
يُعدّ زر "تغريد" في Twitter مثالاً رائعًا على الوظائف التي يمكن تضمينها بشكل أكثر أمانًا في موقعك الإلكتروني من خلال وضع الحماية. تتيح لك منصة Twitter تضمين الزر من خلال iframe باستخدام الرمز التالي:
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
لمعرفة ما يمكننا قفله، دعنا نفحص بعناية الإمكانات التي يتطلبها الزر. ينفّذ رمز HTML الذي يتم تحميله في الإطار جزءًا من JavaScript من خوادم Twitter، وينشئ نافذة منبثقة تتضمّن واجهة تغريد عند النقر عليها. تحتاج هذه الواجهة إلى الوصول إلى ملفات تعريف الارتباط الخاصة بـ Twitter من أجل ربط التغريدة بالحساب الصحيح، وتحتاج إلى القدرة على إرسال نموذج التغريد. هذا كل ما في الأمر، فالإطار لا يحتاج إلى تحميل أي مكوّنات إضافية، ولا يحتاج إلى الانتقال إلى النافذة ذات المستوى الأعلى أو أي عدد من الوظائف الأخرى. بما أنّ الإطار لا يحتاج إلى هذه الامتيازات، لنُزِلها من خلال وضع محتوى الإطار في وضع الحماية.
تعمل آلية وضع الحماية على أساس القائمة البيضاء. نبدأ بإزالة جميع الأذونات الممكنة، ثم نعيد تفعيل الإمكانات الفردية من خلال إضافة علامات محدّدة إلى إعدادات وضع الحماية. بالنسبة إلى تطبيق Twitter المصغّر، قرّرنا تفعيل JavaScript والنوافذ المنبثقة وإرسال النماذج وملفات تعريف الارتباط الخاصة بموقع twitter.com. ويمكننا إجراء ذلك من خلال إضافة سمة sandbox إلى iframe بالقيمة التالية:
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
انتهيت! لقد منحنا الإطار جميع الإمكانات التي يتطلبها، وسيساعد المتصفح في رفض وصوله إلى أي من الامتيازات التي لم نمنحها له صراحةً من خلال قيمة السمة sandbox.
تحكّم دقيق في الإمكانات
لقد رأينا بعض علامات وضع الحماية المحتملة في المثال أعلاه، لنتعرّف الآن على آلية عمل السمة بمزيد من التفصيل.
إذا كان إطار iframe يحتوي على سمة وضع حماية فارغة، سيتم وضع المستند المؤطَّر في وضع الحماية بالكامل، ما يجعله خاضعًا للقيود التالية:
- لن يتم تنفيذ JavaScript في المستند المُضمَّن في إطار. لا يشمل ذلك فقط JavaScript التي يتم تحميلها صراحةً من خلال علامات برمجية، بل يشمل أيضًا معالجات الأحداث المضمّنة وعناوين URL التي تبدأ بـ javascript:. يعني ذلك أيضًا أنّه سيتم عرض المحتوى المضمّن في علامات noscript تمامًا كما لو أنّ المستخدمة قد أوقفت النص البرمجي بنفسها.
- يتم تحميل المستند المؤطّر في مصدر فريد، ما يعني أنّ جميع عمليات التحقّق من المصدر نفسه ستفشل، لأنّ المصادر الفريدة لا تتطابق مع أي مصادر أخرى على الإطلاق، حتى مع نفسها. ومن بين التأثيرات الأخرى، يعني ذلك أنّ المستند لا يمكنه الوصول إلى البيانات المخزّنة في ملفات تعريف الارتباط الخاصة بأي مصدر أو أي آليات تخزين أخرى (تخزين DOM أو Indexed DB أو غير ذلك).
- لا يمكن للمستند المُدرَج في إطار إنشاء نوافذ أو مربّعات حوار جديدة (من خلال
window.openأوtarget="_blank"، على سبيل المثال). - لا يمكن إرسال النماذج.
- لن يتم تحميل المكوّنات الإضافية.
- يمكن للمستند المُدرَج في إطار التنقّل في نفسه فقط، وليس في المستند الرئيسي ذي المستوى الأعلى.
سيؤدي ضبط السمة
window.top.locationإلى ظهور استثناء، ولن يؤدي النقر على الرابط الذي يتضمّن السمةtarget="_top"إلى أي تأثير. - يتم حظر الميزات التي يتم تشغيلها تلقائيًا (مثل عناصر النماذج التي يتم التركيز عليها تلقائيًا، والفيديوهات التي يتم تشغيلها تلقائيًا، وما إلى ذلك).
- يتعذّر قفل المؤشر.
- يتم تجاهل السمة
seamlessفيiframesالتي يحتوي عليها المستند المؤطّر.
هذا الإجراء صارم للغاية، والمستند الذي يتم تحميله في وضع الحماية الكاملة iframe
لا يشكّل سوى خطر ضئيل جدًا. بالطبع، لا يمكنه أيضًا القيام بالكثير من القيمة: أنت
قد تكون قادرًا على الإفلات من وضع الحماية الكامل لبعض المحتوى الثابت، ولكن معظم
في الوقت الذي تريد فيه تخفيف الأمور قليلاً.
باستثناء المكوّنات الإضافية، يمكن رفع كل من هذه القيود من خلال إضافة علامة إلى قيمة سمة وضع الحماية. لا يمكن للمستندات المعزولة في وضع الحماية تشغيل المكوّنات الإضافية، لأنّ المكوّنات الإضافية هي رموز برمجية أصلية غير معزولة في وضع الحماية، ولكن كل شيء آخر مسموح به:
- يسمح
allow-formsبإرسال النماذج. - يسمح
allow-popupsبالنوافذ المنبثقة (يا للمفاجأة!). allow-pointer-lockيسمح بقفل المؤشر.- يسمح
allow-same-originللمستند بالاحتفاظ بمصدره، وستحتفظ الصفحات التي يتم تحميلها منhttps://example.com/بإمكانية الوصول إلى بيانات هذا المصدر. - يسمح
allow-scriptsبتنفيذ JavaScript، كما يسمح بتفعيل الميزات تلقائيًا (لأنّ تنفيذها سيكون بسيطًا من خلال JavaScript). - يسمح
allow-top-navigationللمستند بالخروج من الإطار من خلال الانتقال إلى النافذة ذات المستوى الأعلى.
مع وضع هذه الأمور في الاعتبار، يمكننا تقييم سبب انتهائنا بالضبط بمجموعة محددة من علامات وضع الحماية في مثال Twitter أعلاه:
- يجب استخدام
allow-scriptsلأنّ الصفحة التي يتم تحميلها في الإطار تشغّل بعض نصوص JavaScript البرمجية للتعامل مع تفاعل المستخدم. - يجب استخدام
allow-popupsلأنّ الزر يعرض نموذجًا للتغريد في نافذة جديدة. - يجب استخدام السمة
allow-formsلأنّه يجب أن يكون نموذج التغريد قابلاً للإرسال. allow-same-originضروري، لأنّه بدون ذلك، لن يكون بالإمكان الوصول إلى ملفات تعريف الارتباط الخاصة بموقع twitter.com، ولن يتمكّن المستخدم من تسجيل الدخول لنشر النموذج.
من المهم ملاحظة أنّ علامات وضع الحماية المطبَّقة على إطار معيّن تنطبق أيضًا على أي نوافذ أو إطارات يتم إنشاؤها في وضع الحماية. وهذا يعني أنّه علينا إضافة allow-forms إلى وضع الحماية الخاص بالإطار، على الرغم من أنّ النموذج متوفّر فقط في النافذة التي يظهر فيها الإطار.
باستخدام السمة sandbox، يحصل التطبيق المصغّر على الأذونات التي يحتاجها فقط، وتظل الإمكانات مثل المكوّنات الإضافية والتنقّل في أعلى الصفحة وقفل المؤشر محظورة. لقد قلّلنا من مخاطر تضمين التطبيق المصغّر، بدون أي آثار سلبية.
وهذا الأمر مفيد لجميع الأطراف المعنية.
فصل الامتيازات
من الواضح أنّ عزل المحتوى التابع لجهة خارجية من أجل تشغيل الرموز البرمجية غير الموثوق بها في بيئة منخفضة الامتيازات هو أمر مفيد. ولكن ماذا عن الرمز الخاص بك؟ أنت تثق بنفسك، أليس كذلك؟ إذًا، لماذا يجب الاهتمام بوضع الحماية؟
أودّ طرح السؤال التالي: إذا لم يكن الرمز بحاجة إلى مكوّنات إضافية، لماذا نمنحه إذن الوصول إليها؟ في أفضل الأحوال، هو امتياز لا تستخدمه أبدًا، وفي أسوأ الأحوال، هو ناقل محتمل للمهاجمين للحصول على موطئ قدم. يحتوي رمز كل شخص على أخطاء، وعمليًا كل تطبيق عرضة للاستغلال بطريقة أو بأخرى. يعني وضع الرمز البرمجي في وضع الحماية أنّه حتى إذا نجح المهاجم في تخريب تطبيقك، لن يتم منحه إذن الوصول الكامل إلى مصدر التطبيق، بل سيتمكّن فقط من تنفيذ الإجراءات التي يمكن للتطبيق تنفيذها، وهو أمر سيئ، ولكنه ليس بالسوء الذي يمكن أن يكون عليه.
يمكنك تقليل المخاطر بشكل أكبر من خلال تقسيم تطبيقك إلى أجزاء منطقية وعزل كل جزء بأقل امتياز ممكن. هذه التقنية شائعة جدًا في الرموز البرمجية الأصلية: على سبيل المثال، يقسّم Chrome نفسه إلى عملية متصفّح ذات امتيازات عالية يمكنها الوصول إلى محرك الأقراص الثابتة المحلي ويمكنها إجراء اتصالات بالشبكة، والعديد من عمليات العارض ذات الامتيازات المنخفضة التي تتولى المهمة الصعبة المتمثلة في تحليل المحتوى غير الموثوق به. لا تحتاج أدوات العرض إلى الوصول إلى القرص، لأنّ المتصفّح يتولّى تزويدها بجميع المعلومات التي تحتاج إليها لعرض الصفحة. حتى إذا وجد مخترق ذكي طريقة لإفساد العارض، فلن يتمكن من الوصول إلى أبعد من ذلك، لأن العارض لا يمكنه القيام بالكثير من الأمور المهمة بمفرده: يجب توجيه جميع عمليات الوصول ذات الامتيازات العالية من خلال عملية المتصفح. سيحتاج المهاجمون إلى العثور على عدة ثغرات في أجزاء مختلفة من النظام لإلحاق أي ضرر، ما يقلل بشكل كبير من خطر نجاح الاختراق.
جارٍ عزل eval() بأمان
باستخدام وضع الحماية وواجهة برمجة التطبيقات postMessage، يمكن تطبيق هذا النموذج الناجح على الويب بسهولة. يمكن أن تتوفّر أجزاء من تطبيقك في iframes في وضع الحماية، ويمكن للمستند الرئيسي التوسّط في التواصل بينها من خلال نشر الرسائل والاستماع إلى الردود. يضمن هذا النوع من البنية أن تؤدي الثغرات في أي جزء من التطبيق إلى الحد الأدنى من الضرر الممكن. كما أنّها تجبرك على إنشاء نقاط تكامل واضحة، لتعرف بالضبط أين يجب أن تكون حذرًا بشأن التحقق من صحة المدخلات والمخرجات. لنتناول مثالاً بسيطًا
لنرى كيف يمكن أن يعمل ذلك.
Evalbox هو تطبيق رائع يأخذ سلسلة ويقيّمها على أنّها JavaScript. رائع، أليس كذلك؟ هذا ما كنت تنتظره طوال هذه السنوات الطويلة. بالطبع، هذا تطبيق خطير إلى حدّ ما، لأنّ السماح بتنفيذ JavaScript عشوائي يعني أنّ أي بيانات يقدّمها المصدر تكون متاحة للجميع. سنحدّ من مخاطر حدوث مشاكل من خلال التأكّد من تنفيذ الرمز داخل وضع الحماية، ما يجعله أكثر أمانًا. سنعمل على تحليل الرمز البرمجي من الداخل إلى الخارج، بدءًا من محتوى الإطار:
<!-- frame.html -->
<!DOCTYPE html>
<html>
<head>
<title>Evalbox's Frame</title>
<script>
window.addEventListener('message', function (e) {
var mainWindow = e.source;
var result = '';
try {
result = eval(e.data);
} catch (e) {
result = 'eval() threw an exception.';
}
mainWindow.postMessage(result, event.origin);
});
</script>
</head>
</html>
داخل الإطار، لدينا مستند بسيط يستمع إلى الرسائل من الصفحة الرئيسية من خلال الربط بالحدث message للكائن window.
عندما ينفّذ العنصر الرئيسي postMessage على محتوى iframe، سيتم تشغيل هذا الحدث،
مما يتيح لنا الوصول إلى السلسلة التي يريد العنصر الرئيسي منا
تنفيذها.
في المعالج، نأخذ السمة source للحدث، وهي النافذة الرئيسية. سنستخدم هذا لإرسال نتيجة عملنا الشاق مرة أخرى بمجرد
الانتهاء. بعد ذلك، سنقوم بالمهمة الصعبة، من خلال تمرير البيانات التي تم تقديمها لنا إلى
eval(). This call has been wrapped up in a try block, as banned operations
inside a sandboxed iframe will frequently generate DOM exceptions; we'll catch
those and report a friendly error message instead. أخيرًا، نرسل النتيجة
إلى النافذة الرئيسية. هذا أمر بسيط للغاية.
الوالد غير معقد بالمثل. سننشئ واجهة مستخدم صغيرة تتضمّن textarea
للرمز البرمجي وbutton للتنفيذ، وسنستعين بـ frame.html من خلال
iframe في وضع الحماية، ما يسمح بتنفيذ النصوص البرمجية فقط:
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
id='sandboxed'
src='frame.html'></iframe>
والآن، سنربط الأمور معًا لتنفيذها. أولاً، سنستمع إلى الردود من
iframe وalert() للمستخدمين. من المفترض أن يقوم التطبيق الحقيقي
بشيء أقل إزعاجًا:
window.addEventListener('message',
function (e) {
// Sandboxed iframes which lack the 'allow-same-origin'
// header have "null" rather than a valid origin. This means you still
// have to be careful about accepting data via the messaging API you
// create. Check that source, and validate those inputs!
var frame = document.getElementById('sandboxed');
if (e.origin === "null" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
بعد ذلك، سنربط معالج أحداث بالنقر على button. عندما ينقر المستخدم، سنحصل على المحتوى الحالي لـ textarea، ونمرره إلى الإطار للتنفيذ:
function evaluate() {
var frame = document.getElementById('sandboxed');
var code = document.getElementById('code').value;
// Note that we're sending the message to "*", rather than some specific
// origin. Sandboxed iframes which lack the 'allow-same-origin' header
// don't have an origin which you can target: you'll have to send to any
// origin, which might alow some esoteric attacks. Validate your output!
frame.contentWindow.postMessage(code, '*');
}
document.getElementById('safe').addEventListener('click', evaluate);
الأمر سهل، أليس كذلك؟ لقد أنشأنا واجهة برمجة تطبيقات تقييم بسيطة للغاية، ويمكننا التأكد من أنّ الرمز الذي يتم تقييمه لا يمكنه الوصول إلى المعلومات الحساسة مثل ملفات تعريف الارتباط أو مساحة تخزين DOM. وبالمثل، لا يمكن للرمز الذي تم تقييمه تحميل المكوّنات الإضافية أو النوافذ المنبثقة الجديدة أو أي عدد من الأنشطة المزعجة أو الضارة الأخرى.
يمكنك فعل الشيء نفسه مع التعليمات البرمجية الخاصة بك عن طريق تقسيم التطبيقات المتجانسة إلى مكونات أحادية الغرض. ويمكن تضمين كل منها في واجهة برمجة تطبيقات بسيطة للمراسلة، تمامًا مثل ما كتبناه أعلاه. يمكن أن تعمل النافذة الرئيسية ذات الامتيازات العالية كوحدة تحكّم ومُرسِل، حيث ترسل الرسائل إلى وحدات معيّنة يمتلك كل منها أقل عدد ممكن من الامتيازات لأداء وظائفها، وتستمع إلى النتائج، وتضمن أن يتم تزويد كل وحدة بالمعلومات التي تتطلبها فقط.
ومع ذلك، يجب توخي الحذر الشديد عند التعامل مع المحتوى المؤطّر
الذي يأتي من المصدر نفسه الذي يأتي منه الأصل. إذا كانت صفحة على https://example.com/ تضع إطارًا لصفحة أخرى على المصدر نفسه باستخدام وضع الحماية الذي يتضمّن كلاً من العلامتَين allow-same-origin وallow-scripts، يمكن للصفحة المؤطَّرة الوصول إلى الصفحة الرئيسية وإزالة سمة وضع الحماية بالكامل.
العب في وضع الحماية
تتوفّر ميزة وضع الحماية الآن في مجموعة متنوعة من المتصفّحات: Firefox 17 والإصدارات الأحدث وIE10 والإصدارات الأحدث وChrome في وقت كتابة هذه المقالة (يحتوي موقع caniuse بالطبع على جدول دعم محدّث). يتيح لك تطبيق السمة sandbox على iframes التي تدرجها منح امتيازات معيّنة للمحتوى الذي تعرضه، فقط تلك الامتيازات الضرورية لكي يعمل المحتوى بشكل صحيح. يمنحك ذلك الفرصة للحدّ من المخاطر المرتبطة بتضمين محتوى تابع لجهة خارجية، بما يتجاوز ما هو ممكن حاليًا من خلال سياسة أمان المحتوى.
علاوة على ذلك، فإن وضع الحماية هو أسلوب قوي لتقليل مخاطر تمكّن المهاجم الذكي من استغلال الثغرات في الرمز الخاص بك. من خلال فصل تطبيق متكامل إلى مجموعة من الخدمات المعزولة، كل منها مسؤول عن جزء صغير من الوظائف المستقلة، سيضطر المهاجمون ليس فقط إلى اختراق محتوى إطارات معينة، ولكن أيضًا وحدة التحكم الخاصة بها. هذه مهمة أكثر صعوبة، خاصةً أنه يمكن تقليل نطاق وحدة التحكم بشكل كبير. يمكنك بذل جهدك في تدقيق هذا الرمز البرمجي إذا طلبت من المتصفّح المساعدة في الباقي.
هذا لا يعني أنّ وضع الحماية هو حلّ كامل لمشكلة الأمان على الإنترنت. ويوفّر هذا الإجراء دفاعًا في العمق، ولكن لا يمكنك الاعتماد على دعم المتصفّح لجميع المستخدمين ما لم يكن لديك إمكانية التحكّم في برامج المستخدمين (إذا كنت تتحكّم في برامج المستخدمين، في بيئة مؤسسة مثلاً، فهذا رائع). يومًا ما… ولكن في الوقت الحالي، تُعدّ تقنية وضع الحماية طبقة أخرى من الحماية لتعزيز دفاعاتك، وليست دفاعًا كاملاً يمكنك الاعتماد عليه بشكل كامل. ومع ذلك، فإن الطبقات ممتازة. أقترح الاستفادة من هذا.
محتوى إضافي للقراءة
"Privilege Separation in HTML5 Applications" هي ورقة بحثية مثيرة للاهتمام تتناول تصميم إطار عمل صغير، وتطبيقه على ثلاثة تطبيقات HTML5 حالية.
يمكن أن يكون وضع الحماية أكثر مرونة عند دمجه مع سمتَي iframe الجديدتين:
srcdocوseamless. يسمح لك الخيار الأول بملء إطار بالمحتوى بدون عبء طلب HTTP، ويسمح الخيار الأخير بتدفق النمط إلى المحتوى المؤطر. كلاهما يحظى بدعم ضعيف من المتصفحات في الوقت الحالي (Chrome وWebKit nightlies)، ولكنهما سيشكلان مزيجًا مثيرًا للاهتمام في المستقبل. على سبيل المثال، يمكنك عزل التعليقات على مقالة معيّنة من خلال الرمز التالي:<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>