מערכת התוספים של Chrome אוכפת מדיניות ברירת מחדל (CSP) די מחמירה.
ההגבלות של המדיניות הן פשוטות: צריך להעביר את הסקריפט מחוץ לשורה לקובצי JavaScript נפרדים, להמיר את הפונקציות לטיפול באירועים בשורה כך שישתמשו ב-addEventListener, ולהשבית את eval().
עם זאת, אנחנו מודעים לכך שבספריות שונות נעשה שימוש במבנים דומים ל-eval() ול-eval, כמו new Function(), כדי לבצע אופטימיזציה של הביצועים ולפשט את הביטויים. ספריות של תבניות במיוחד נוטות לסגנון ההטמעה הזה. יש כמה (כמו Angular.js) שתומכים ב-CSP מחוץ לקופסה, אבל הרבה מסגרות פופולריות עדיין לא עודכנו למנגנון שתואם לעולם eval-less של התוספים. לכן, הסרת התמיכה בפונקציונליות הזו הייתה בעייתית יותר מהצפוי עבור מפתחים.
במאמר הזה נסביר על ארגז חול (sandboxing) כמנגנון בטוח לשילוב הספריות האלה בפרויקטים שלכם בלי לפגוע באבטחה.
למה כדאי להשתמש בארגז חול?
השימוש ב-eval מסוכן בתוך תוסף כי לקוד שהוא מפעיל יש גישה לכל מה שנמצא בסביבה עם ההרשאות הגבוהות של התוסף. יש מגוון רחב של ממשקי API עוצמתיים של browser.* שיכולים להשפיע באופן משמעותי על האבטחה והפרטיות של המשתמשים. חילוץ פשוט של נתונים הוא הבעיה הכי פחות חמורה.
הפתרון המוצע הוא ארגז חול שבו eval יכול להריץ קוד בלי גישה לנתונים של התוסף או לממשקי ה-API בעלי הערך הגבוה של התוסף. אין נתונים, אין ממשקי API, אין בעיה.
אנחנו עושים את זה על ידי רישום של קובצי HTML ספציפיים בחבילת התוסף כקובצי sandbox.
בכל פעם שנטען דף בסביבת ארגז חול, הוא יועבר אל מקור ייחודי, והגישה שלו לממשקי browser.* API תיחסם. אם נטען את הדף הזה בסביבת ארגז חול לתוסף שלנו באמצעות iframe, נוכל להעביר לו הודעות, לאפשר לו לפעול על ההודעות האלה בצורה מסוימת ולחכות שהוא יעביר לנו בחזרה תוצאה. מנגנון ההודעות הפשוט הזה מספק לנו את כל מה שצריך כדי לכלול בבטחה קוד מבוסס-eval בתהליך העבודה של התוסף.
יצירה ושימוש בסביבת ארגז חול
אם אתם רוצים להתחיל לכתוב קוד מיד, אתם יכולים להוריד את תוסף הדוגמה של ארגז חול ולהתחיל לעבוד. זו דוגמה פעילה של API קטן להעברת הודעות שנבנה על בסיס ספריית התבניות 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'].
העברת התוצאה בחזרה
כדי לאפשר שימוש בתבנית הזו, נגדיר פונקציית message listener שמקבלת פקודות מדף התוסף. נשתמש ב-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 לא יוצרת סיכון אבטחה משמעותי, כי אנחנו בוטחים בתוכן שעבר עיבוד בתוך ארגז החול.
המנגנון הזה מאפשר ליצור תבניות בקלות, אבל הוא לא מוגבל רק ליצירת תבניות. אפשר להריץ בארגז חול כל קוד שלא פועל מחוץ לקופסה במסגרת Content Security Policy מחמירה. למעשה, לעיתים קרובות כדאי להריץ בארגז חול רכיבים של התוספים שיפעלו בצורה תקינה כדי להגביל כל חלק בתוכנית לסט ההרשאות המינימלי שנדרש כדי שהיא תפעל בצורה תקינה. במצגת Writing Secure Web Apps and Chrome Extensions מ-Google I/O 2012 יש כמה דוגמאות טובות לטכניקה הזו בפעולה, וכדאי להקדיש לה 56 דקות.