שימוש ב-eval() במסגרות iframe שבארגז חול (sandbox)

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

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

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

למה כדאי להשתמש ב-Sandbox?

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

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

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

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

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

כל קובץ שאמור לפעול בתוך ארגז חול חייב להופיע במניפסט של התוסף. לשם כך, צריך להוסיף מאפיין sandbox. זהו שלב קריטי וקל לשכוח אותו, לכן חשוב לוודא שהקובץ שבארגז החול מופיע במניפסט. בדוגמה הזו אנחנו מבצעים הרצה בארגז חול (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, ויוצרת ומהדרת תבנית מוטבעת באופן שבו סרגלי האחיזה מציע:

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