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