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

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

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

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

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

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

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

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

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

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

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

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

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

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

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