يفرض نظام الإضافات في Chrome سياسة أمان محتوى (CSP) تلقائية صارمة إلى حدّ ما.
قيود السياسة واضحة: يجب نقل النص البرمجي خارج السطر إلى ملفات JavaScript منفصلة، ويجب تحويل معالجات الأحداث المضمّنة لاستخدام addEventListener، ويجب إيقاف eval().
ومع ذلك، ندرك أنّ مجموعة متنوعة من المكتبات تستخدم بنيات مشابهة لـ eval() وeval، مثل new Function()، لتحسين الأداء وتسهيل التعبير. وتكون مكتبات إنشاء النماذج عرضة بشكل خاص لهذا النوع من التنفيذ. في حين أنّ بعض الأطر (مثل Angular.js) تتوافق مع سياسة أمان المحتوى بدون أي تعديل، لم يتم بعد تعديل العديد من الأطر الشائعة لتتوافق مع الآلية التي لا تتضمّن eval في الإضافات. لذلك، تبيّن أنّ إزالة إمكانية استخدام هذه الوظيفة أكثر إشكالية من المتوقّع بالنسبة إلى المطوّرين.
يقدّم هذا المستند وضع الحماية كآلية آمنة لتضمين هذه المكتبات في مشاريعك بدون المساس بالأمان.
لماذا وضع الحماية؟
eval خطير داخل الإضافة لأنّ الرمز الذي يتم تنفيذه يمكنه الوصول إلى كل شيء في بيئة الإضافة التي تتطلّب أذونات عالية. تتوفّر مجموعة كبيرة من واجهات برمجة التطبيقات browser.* القوية التي يمكن أن تؤثّر بشكل كبير في أمان المستخدم وخصوصيته، علمًا بأنّ سرقة البيانات البسيطة هي أقل ما يثير قلقنا.
الحلّ المقترَح هو بيئة اختبارية يمكن أن ينفّذ فيها eval الرمز البرمجي بدون الوصول إلى بيانات الإضافة أو واجهات برمجة التطبيقات القيّمة الخاصة بها. لا بيانات ولا واجهات برمجة تطبيقات، ولا مشكلة.
ونحقق ذلك من خلال إدراج ملفات HTML معيّنة داخل حزمة الإضافة على أنّها معزولة.
عند تحميل صفحة في وضع الحماية، سيتم نقلها إلى مصدر فريد، وسيتم حظر وصولها إلى واجهات برمجة التطبيقات browser.*. إذا حمّلنا هذه الصفحة المحمية في وضع الحماية إلى إضافتنا من خلال iframe، يمكننا إرسال رسائل إليها والسماح لها بالتفاعل مع هذه الرسائل بطريقة ما، ثم انتظار أن ترسل إلينا نتيجة. تمنحنا آلية المراسلة البسيطة هذه كل ما نحتاج إليه لتضمين الرمز البرمجي المستند إلى eval بأمان في سير عمل الإضافة.
إنشاء بيئة اختبار واستخدامها
إذا أردت الانتقال مباشرةً إلى الرمز، يمكنك الحصول على نموذج إضافة وضع الحماية والتجربة. وهو مثال عملي على واجهة برمجة تطبيقات صغيرة للمراسلة تم إنشاؤها استنادًا إلى مكتبة نماذج Handlebars، ومن المفترض أن يوفّر لك كل ما تحتاج إليه لبدء الاستخدام. إذا أردت الحصول على مزيد من التوضيح، لنستعرض معًا هذا المثال هنا.
إدراج الملفات في ملف البيان
يجب إدراج كل ملف يجب تشغيله داخل بيئة الاختبار المعزولة في بيان الإضافة من خلال إضافة السمة sandbox. هذه خطوة مهمة، ومن السهل نسيانها، لذا تأكَّد من أنّ ملفك الموضوع في وضع الحماية مدرَج في ملف البيان. في هذا المثال، نضع الملف الذي يحمل الاسم الذكي "sandbox.html" في بيئة الاختبار المعزولة. يبدو إدخال البيان على النحو التالي:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
تحميل الملف في وضع الحماية
ولإجراء عملية مثيرة للاهتمام على الملف المحمي، علينا تحميله في سياق يمكن فيه أن يتم التعامل معه من خلال رمز الإضافة. في هذا المثال، تم تحميل sandbox.html في صفحة إضافة باستخدام iframe. يحتوي ملف JavaScript الخاص بالصفحة على رمز يُرسِل رسالة إلى وضع الحماية كلما تم النقر على إجراء المتصفح من خلال العثور على iframe في الصفحة واستدعاء postMessage() على contentWindow. الرسالة هي عنصر يحتوي على ثلاث سمات: context وtemplateName وcommand. سنتناول context وcommand بالتفصيل بعد قليل.
service-worker.js:
browser.action.onClicked.addListener(() => {
browser.tabs.create({
url: 'mainpage.html'
});
console.log('Opened a tab with a sandboxed page!');
});
extension-page.js:
let counter = 0;
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('reset').addEventListener('click', function () {
counter = 0;
document.querySelector('#result').innerHTML = '';
});
document.getElementById('sendMessage').addEventListener('click', function () {
counter++;
let message = {
command: 'render',
templateName: 'sample-template-' + counter,
context: { counter: counter }
};
document.getElementById('theFrame').contentWindow.postMessage(message, '*');
});
القيام بشيء خطير
عند تحميل sandbox.html، يتم تحميل مكتبة Handlebars وإنشاء وتجميع نموذج مضمّن بالطريقة التي تقترحها Handlebars:
extension-page.html:
<!DOCTYPE html>
<html>
<head>
<script src="mainpage.js"></script>
<link href="styles/main.css" rel="stylesheet" />
</head>
<body>
<div id="buttons">
<button id="sendMessage">Click me</button>
<button id="reset">Reset counter</button>
</div>
<div id="result"></div>
<iframe id="theFrame" src="sandbox.html" style="display: none"></iframe>
</body>
</html>
sandbox.html:
<script id="sample-template-1" type="text/x-handlebars-template">
<div class='entry'>
<h1>Hello</h1>
<p>This is a Handlebar template compiled inside a hidden sandboxed
iframe.</p>
<p>The counter parameter from postMessage() (outer frame) is:
</p>
</div>
</script>
<script id="sample-template-2" type="text/x-handlebars-template">
<div class='entry'>
<h1>Welcome back</h1>
<p>This is another Handlebar template compiled inside a hidden sandboxed
iframe.</p>
<p>The counter parameter from postMessage() (outer frame) is:
</p>
</div>
</script>
لن يحدث ذلك! على الرغم من أنّ Handlebars.compile تستخدم new Function، تعمل الأمور كما هو متوقّع تمامًا، وينتهي بنا الأمر بنموذج مجمّع في templates['hello'].
إعادة النتيجة
سنوفّر هذا النموذج لاستخدامه من خلال إعداد أداة معالجة الرسائل التي تقبل الأوامر من صفحة الإضافة. سنستخدم command الذي تم تمريره لتحديد الإجراءات التي يجب اتّخاذها (يمكنك تخيّل تنفيذ إجراءات أكثر من مجرد العرض، مثل إنشاء قوالب). ربما تتم إدارتها بطريقة ما)، وسيتم تمرير context إلى النموذج مباشرةً لعرضه. سيتم إعادة محتوى HTML المعروض إلى صفحة الإضافة كي تتمكّن الإضافة من الاستفادة منه لاحقًا:
<script>
const templatesElements = document.querySelectorAll(
"script[type='text/x-handlebars-template']"
);
let templates = {},
source,
name;
// precompile all templates in this page
for (let i = 0; i < templatesElements.length; i++) {
source = templatesElements[i].innerHTML;
name = templatesElements[i].id;
templates[name] = Handlebars.compile(source);
}
// Set up message event handler:
window.addEventListener('message', function (event) {
const command = event.data.command;
const template = templates[event.data.templateName];
let result = 'invalid request';
// if we don't know the templateName requested, return an error message
if (template) {
switch (command) {
case 'render':
result = template(event.data.context);
break;
// you could even do dynamic compilation, by accepting a command
// to compile a new template instead of using static ones, for example:
// case 'new':
// template = Handlebars.compile(event.data.templateSource);
// result = template(event.data.context);
// break;
}
} else {
result = 'Unknown template: ' + event.data.templateName;
}
event.source.postMessage({ result: result }, event.origin);
});
</script>
في صفحة الإضافة، ستصلنا هذه الرسالة، وسنتخذ إجراءً مثيرًا للاهتمام بشأن html
البيانات التي تم تمريرها إلينا. في هذه الحالة، سنعرضها ببساطة من خلال إشعار، ولكن من الممكن تمامًا استخدام هذا الترميز HTML بأمان كجزء من واجهة مستخدم الإضافة. لا يشكّل إدراجها من خلال
innerHTML خطرًا أمنيًا كبيرًا لأنّنا نثق بالمحتوى الذي تم عرضه
داخل البيئة التجريبية.
تسهّل هذه الآلية إنشاء النماذج، ولكنّها بالطبع لا تقتصر على ذلك. يمكن وضع أي رمز لا يعمل بشكل مباشر ضمن سياسة صارمة لأمان المحتوى في بيئة معزولة. وفي الواقع، من المفيد غالبًا وضع مكوّنات الإضافات التي ستعمل بشكل صحيح في بيئة معزولة من أجل حصر كل جزء من برنامجك في أصغر مجموعة من الامتيازات اللازمة لتنفيذه بشكل صحيح. تقدّم презентация Writing Secure Web Apps and Chrome Extensions من مؤتمر Google I/O لعام 2012 بعض الأمثلة الجيدة على هذه التقنية أثناء العمل، وهي تستحق 56 دقيقة من وقتك.