سیستم افزونههای کروم یک سیاست امنیتی محتوای پیشفرض (CSP) نسبتاً سختگیرانهای را اعمال میکند. محدودیتهای این سیاست ساده هستند: اسکریپت باید به صورت خارج از خط به فایلهای جاوا اسکریپت جداگانه منتقل شود، کنترلکنندههای رویداد درونخطی باید به گونهای تبدیل شوند که از addEventListener استفاده کنند و eval() غیرفعال است.
با این حال، ما میدانیم که انواع کتابخانهها از eval() و ساختارهای شبیه eval مانند new Function() برای بهینهسازی عملکرد و سهولت بیان استفاده میکنند. کتابخانههای قالببندی به ویژه مستعد این سبک پیادهسازی هستند. در حالی که برخی (مانند Angular.js ) از CSP به صورت پیشفرض پشتیبانی میکنند، بسیاری از چارچوبهای محبوب هنوز به مکانیسمی که با دنیای بدون eval افزونهها سازگار باشد، بهروزرسانی نشدهاند. بنابراین، حذف پشتیبانی از این قابلیت برای توسعهدهندگان بیش از آنچه انتظار میرفت مشکلساز بوده است.
این سند، سندباکسینگ را به عنوان یک مکانیسم امن برای گنجاندن این کتابخانهها در پروژههای شما بدون به خطر انداختن امنیت معرفی میکند.
چرا سندباکس؟
eval درون یک افزونه خطرناک است زیرا کدی که اجرا میکند به همه چیز در محیط با مجوز بالای افزونه دسترسی دارد. تعداد زیادی chrome.* APIهایی در دسترس هستند که میتوانند به شدت بر امنیت و حریم خصوصی کاربر تأثیر بگذارند؛ استخراج ساده دادهها کمترین نگرانی ماست. راهحل ارائه شده یک جعبه شنی است که در آن eval میتواند کد را بدون دسترسی به دادههای افزونه یا APIهای با ارزش بالای افزونه اجرا کند. بدون داده، بدون API، مشکلی نیست.
ما این کار را با فهرست کردن فایلهای HTML خاص درون بسته افزونه به عنوان فایلهای sandbox انجام میدهیم. هر زمان که یک صفحه sandbox شده بارگذاری شود، به یک مبدأ منحصر به فرد منتقل میشود و دسترسی به رابطهای برنامهنویسی chrome.* از آن سلب میشود. اگر این صفحه sandbox شده را از طریق یک iframe در افزونه خود بارگذاری کنیم، میتوانیم پیامهایی را به آن ارسال کنیم، اجازه دهیم به نحوی بر اساس آن پیامها عمل کند و منتظر بمانیم تا نتیجهای را به ما بازگرداند. این مکانیسم پیامرسانی ساده، هر آنچه را که برای گنجاندن ایمن کد مبتنی بر eval در گردش کار افزونه خود نیاز داریم، در اختیار ما قرار میدهد.
ایجاد و استفاده از یک سندباکس
اگر میخواهید مستقیماً وارد کدنویسی شوید، افزونه نمونه sandboxing را بردارید و شروع کنید . این یک نمونه کار از یک API پیامرسان کوچک است که بر روی کتابخانه قالببندی Handlebars ساخته شده است و باید هر آنچه را که برای شروع کار نیاز دارید، در اختیار شما قرار دهد. برای آن دسته از شما که توضیح بیشتری میخواهید، بیایید در اینجا با هم آن نمونه را بررسی کنیم.
لیست کردن فایلها در مانیفست
هر فایلی که قرار است درون یک سندباکس اجرا شود، باید با اضافه کردن یک ویژگی sandbox در مانیفست افزونه فهرست شود. این یک مرحله حیاتی است و به راحتی ممکن است فراموش شود، بنابراین دوباره بررسی کنید که فایل سندباکس شده شما در مانیفست فهرست شده باشد. در این نمونه، ما فایلی را که هوشمندانه "sandbox.html" نامگذاری شده است، سندباکس میکنیم. ورودی مانیفست به این شکل است:
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
فایل سندباکس شده را بارگذاری کنید
برای انجام کاری جالب با فایل sandbox شده، باید آن را در بستری بارگذاری کنیم که بتوان آن را توسط کد افزونه آدرسدهی کرد. در اینجا، sandbox.html با استفاده از یک iframe در یک صفحه افزونه بارگذاری شده است. فایل جاوا اسکریپت صفحه حاوی کدی است که هر زمان که مرورگر روی عملکرد iframe در صفحه کلیک کند، با یافتن iframe در صفحه و فراخوانی postMessage() در contentWindow آن، پیامی را به sandbox ارسال میکند. این پیام یک شیء است که شامل سه ویژگی است: context ، templateName و command . در ادامه به context و command خواهیم پرداخت.
سرویس-ورکر.js:
chrome.action.onClicked.addListener(() => {
chrome.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 پیشنهاد میدهد، ایجاد و کامپایل میکند:
صفحه-افزونه.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>
سندباکس.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 خطر امنیتی قابل توجهی ایجاد نمیکند زیرا ما به محتوایی که در sandbox رندر شده است، اعتماد داریم.
این مکانیزم، قالببندی را ساده میکند، اما البته محدود به قالببندی نیست. هر کدی که تحت یک سیاست امنیتی محتوای سختگیرانه به طور خودکار کار نمیکند، میتواند در جعبه شنی قرار گیرد؛ در واقع، اغلب مفید است که اجزای افزونههای خود را که به درستی اجرا میشوند ، در جعبه شنی قرار دهید تا هر بخش از برنامه خود را به کوچکترین مجموعه امتیازات لازم برای اجرای صحیح آن محدود کنید. ارائه «نوشتن برنامههای وب امن و افزونههای کروم» از Google I/O 2012، نمونههای خوبی از این تکنیکها را در عمل ارائه میدهد و ارزش 56 دقیقه وقت شما را دارد.