שימוש ב-eval בתוספים ל-Chrome

מערכת התוספים של Chrome אוכפת מדיניות אבטחת תוכן (CSP) מחמירה למדי כברירת מחדל. ההגבלות של המדיניות הן פשוטות: יש להעביר את הסקריפט מחוץ למסגרת למדיניות נפרדת קובצי JavaScript, צריך להמיר גורמים מטפלים באירועים מוטבעים כדי להשתמש ב-addEventListener, והמדיניות eval() היא מושבת. לאפליקציות Chrome יש מדיניות מחמירה עוד יותר, ואנחנו די מרוצים מהאבטחה הנכסים שכללי המדיניות האלה מספקים.

עם זאת, אנחנו מבינים שבמגוון ספריות נעשה שימוש במבנים דמויי eval() וכמו eval, כמו new Function() לאופטימיזציה של הביצועים וקלות הביטוי. ספריות תבניות נוטות במיוחד לסגנון הטמעה כזה. חלק מהמסגרות (כמו Angular.js) תומכות ב-CSP כברירת מחדל, אבל הרבה מסגרות פופולריות עדיין לא עודכנו למנגנון שתואם לעולם בלי eval של התוספים. לכן, הסרת התמיכה בפונקציונליות הזו התבררה כבעייתית יותר מהצפוי למפתחים.

במסמך הזה אנחנו מציגים את ארגז החול כמנגנון בטוח להכללת הספריות האלה בפרויקטים שלכם. בלי לפגוע באבטחה. כדי לקצר את הטקסט, נשתמש במונח תוספים לכל אורך המאמר, אבל העקרון רלוונטי גם לאפליקציות.

למה כדאי להשתמש בארגז חול?

eval מסוכן בתוך תוסף כי לקוד שהוא מבצע יש גישה לכל מה שבסביבת ההרשאות הגבוהות של התוסף. יש המון ממשקי API חזקים של chrome.* שיכולים לעזור ישפיעו באופן חמור על האבטחה והפרטיות של המשתמשים. זליגת מידע פשוטה היא המעטה ביותר בדאגות שלנו. הפתרון המוצע הוא ארגז חול שבו eval יכול להריץ קוד בלי גישה לנתונים של התוסף או לממשקי ה-API החשובים של התוסף. אין נתונים, אין ממשקי API, אין בעיה.

כדי לעשות זאת, אנחנו מציינים קובצי HTML ספציפיים בחבילת התוסף כקבצים שנמצאים בארגז חול. בכל פעם שדף שפועל בארגז החול נטען, הוא מועבר למקור ייחודי, והוא יידחה גישה לממשקי API של chrome.*. אם נטען את הדף הזה בקונטיינר של חול באמצעות iframe, נוכל להעביר לו הודעות, לאפשר לו לבצע פעולות כלשהן על ההודעות האלה ולהמתין שהוא יחזיר לנו תוצאה. המנגנון הפשוט הזה להעברת הודעות נותן לנו את כל מה שאנחנו צריכים כדי לכלול בבטחה הודעות מ-eval בתהליך העבודה של התוסף שלנו.

יצירה של ארגז חול ושימוש בו.

רוצים לצלול ישר אל הקוד? קחו את התוסף לדוגמה של Sandboxing מושבת. זוהי דוגמה בפועל לממשק API קטנטן להעברת הודעות המבוסס על סרגלי היד ליצירת תבניות, והיא אמורה לספק את כל מה שנחוץ כדי להתחיל. לאלה מכם ש כמו הסבר נוסף, בואו נעבור על הדוגמה הזו יחד כאן.

הצגת רשימה של קבצים במניפסט

כל קובץ שצריך להריץ בתוך ארגז חול צריך להופיע במניפסט של התוסף על ידי הוספת מאפיין sandbox. זהו שלב קריטי שקל לשכוח, לכן חשוב לוודא שהקובץ ב-sandbox מופיע במניפסט. בדוגמה הזו אנחנו מריצים את הקובץ בצורה חכמה בארגז חול שנקרא 'sandbox.html'. הרשומה במניפסט נראית כך:

{
  ...,
  "sandbox": {
     "pages": ["sandbox.html"]
  },
  ...
}

טעינת הקובץ בארגז החול

כדי לעשות משהו מעניין עם הקובץ ב-sandbox, צריך לטעון אותו בהקשר שבו אפשר לטפל בו באמצעות הקוד של התוסף. כאן, sandbox.html נטען דף האירוע של התוסף (eventpage.html) באמצעות iframe. eventpage.js מכיל קוד ששולחת הודעה לארגז החול בכל פעם שמשתמש לוחץ על פעולת הדפדפן על ידי איתור השדה iframe בדף, והפעלת ה-method postMessage ב-contentWindow שלו. ההודעה היא אובייקט שמכיל שני מאפיינים: context ו-command. מיד נתעמק בשתיהן.

chrome.browserAction.onClicked.addListener(function() {
 var iframe = document.getElementById('theFrame');
 var message = {
   command: 'render',
   context: {thing: 'world'}
 };
 iframe.contentWindow.postMessage(message, '*');
});
למידע כללי על ה-API של postMessage, אפשר לעיין במסמכי העזרה של postMessage ב-MDN. הוא מלא וכדאי לקרוא אותו. חשוב לזכור שאפשר להעביר נתונים הלוך ושוב רק אם הם ניתנים לסריאליזציה. פונקציות, למשל, הן לא פונקציות.

ביצוע פעולה מסוכנת

כשטוענים את sandbox.html, היא טוענת את ספריית הכינויים, ויוצרת ויוצרת הודעה באופן שבו סרגלי היד מציעים:

<script src="handlebars-1.0.0.beta.6.js"></script>
<script id="hello-world-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>Hello, !</h1>
  </div>
</script>
<script>
  var templates = [];
  var source = document.getElementById('hello-world-template').innerHTML;
  templates['hello'] = Handlebars.compile(source);
</script>

הפקודה הזו לא נכשלת. למרות ש-Handlebars.compile משתמש ב-new Function, הכול עובד בדיוק כצפוי, ובסוף מתקבלת תבנית מתומצתת ב-templates['hello'].

החזרת התוצאה

כדי שהתבנית הזו תהיה זמינה לשימוש, צריך להגדיר האזנה להודעות שמקבלת פקודות מדף האירוע. נשתמש בcommand שהועברה כדי לקבוע מה צריך לעשות (יכול להיות דמיינו לעשות דברים נוספים מלבד רינדור, אולי ליצור תבניות? אולי כדאי לנהל אותם בדרך?), והcontext יועבר ישירות לתבנית לצורך רינדור. קוד ה-HTML שעבר עיבוד תועבר חזרה לדף האירוע כדי שהתוסף יוכל להשתמש בו כדי להפיק ממנו תועלת בהמשך:

<script>
  window.addEventListener('message', function(event) {
    var command = event.data.command;
    var name = event.data.name || 'hello';
    switch(command) {
      case 'render':
        event.source.postMessage({
          name: name,
          html: templates[name](event.data.context)
        }, event.origin);
        break;

      // case 'somethingElse':
      //   ...
    }
  });
</script>

בדף האירוע, נקבל את ההודעה הזו ונבצע פעולה מעניינת עם html הנתונים שהועברו אלינו. במקרה כזה, פשוט נשלחה אותו באמצעות התראה למחשב, אבל אפשר להשתמש ב-HTML הזה בבטחה כחלק מממשק המשתמש של התוסף. הוספה דרך האפליקציה innerHTML לא מהווה סיכון אבטחה משמעותי, גם כסכנה מלאה של ארגז החול (sandboxing) קוד דרך כמה מתקפה חכמה לא יוכל להחדיר תוכן מסוכן של סקריפט או פלאגין הקשר של תוסף בעל הרשאות גבוהות.

המנגנון הזה מאפשר ליצור תבניות בקלות, אבל הוא כמובן לא מוגבל ליצירת תבניות. אפשר להריץ בארגז חול כל קוד שלא פועל באופן אוטומטי במסגרת מדיניות אבטחת תוכן מחמירה. למעשה, לרוב כדאי להריץ בארגז חול רכיבים של התוספים שאמורים לפעול בצורה תקינה, כדי להגביל כל חלק בתוכנית לקבוצת ההרשאות הקטנה ביותר שנדרשת כדי שהוא יפעל כראוי. כדאי להקדיש 56 דקות כדי לצפות בהרצאה כתיבה של אפליקציות אינטרנט מאובטחות ותוספים ל-Chrome מ-Google I/O 2012, שבה מפורטות דוגמאות טובות לשימוש בשיטות האלה.