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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

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