משחקים בבטחה ב-IFrames בארגז חול

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

מדיניות אבטחת תוכן (CSP) יכולה לצמצם את הסיכונים שקשורים לשני סוגי התוכן האלה, כי היא מאפשרת לכם להוסיף לרשימת ההיתרים מקורות מהימנים ספציפיים של סקריפטים ותוכן אחר. זהו צעד חשוב בכיוון הנכון, אבל חשוב לציין שההגנה שרוב ההנחיות של CSP מציעות היא בינארית: המשאב מותר או לא. לפעמים כדאי לומר "אני לא בטוח/ה שאני באמת סומך/ת על מקור התוכן הזה, אבל הוא כל כך יפה! הטמע אותו, בבקשה, דפדפן, אבל אל תיתן לו לשבור את האתר שלי".

הרשאות מינימליות

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

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

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

Twust, but verify

הכפתור "Tweet" של טוויטר הוא דוגמה מצוינת לפונקציונליות שאפשר להטמיע באתר בצורה בטוחה יותר באמצעות ארגז חול. בטוויטר אפשר להטמיע את הלחצן באמצעות iframe עם הקוד הבא:

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

כדי להבין מה אפשר לנעול, נבדוק בקפידה אילו יכולות נדרשות לכפתור. קוד ה-HTML שנטען לתוך המסגרת מריץ קטע קוד של JavaScript מהשרתים של Twitter, ויוצר חלון קופץ עם ממשק לציוץ כשלוחצים עליו. הממשק הזה צריך גישה לקובצי ה-Cookie של Twitter כדי לקשר את הציוץ לחשבון הנכון, וצריך את היכולת לשלוח את טופס הציוץ. זה פחות או יותר הכול. לא צריך לטעון פלאגינים ב-frame, לא צריך לנווט בחלון ברמה העליונה או להשתמש בפונקציונליות אחרת. הוא לא צריך את ההרשאות האלה, אז נסיר אותן על ידי הפעלת התוכן של המסגרת בארגז חול.

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

<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
    src="https://platform.twitter.com/widgets/tweet_button.html"
    style="border: 0; width:130px; height:20px;"></iframe>

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

שליטה מדויקת ביכולות

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

אם יש iframe עם מאפיין sandbox ריק, המסמך הממוסגר יהיה בארגז חול (sandbox) מלא, והוא יהיה כפוף להגבלות הבאות:

  • ‫JavaScript לא יפעל במסמך הממוסגר. ההגדרה הזו כוללת לא רק JavaScript שנטען באופן מפורש באמצעות תגי סקריפט, אלא גם גורמים מטפלים באירועים בתוך השורה וכתובות URL של javascript:. המשמעות היא גם שתוכן שכלול בתגי noscript יוצג, בדיוק כאילו המשתמשת השביתה את הסקריפט בעצמה.
  • המסמך הממוסגר נטען למקור ייחודי, כלומר כל הבדיקות של אותו מקור ייכשלו. מקורות ייחודיים לא תואמים לאף מקור אחר, אפילו לא לעצמם. בין היתר, המשמעות היא שלמסמך אין גישה לנתונים שמאוחסנים בקובצי Cookie של מקור כלשהו או במנגנוני אחסון אחרים (אחסון DOM,‏ Indexed DB וכו').
  • המסמך הממוסגר לא יכול ליצור חלונות או תיבות דו-שיח חדשים (למשל, באמצעות window.open או target="_blank").
  • אי אפשר לשלוח את הטופס.
  • התוספים לא ייטענו.
  • המסמך הממוסגר יכול לנווט רק בתוך עצמו, ולא בתוך ההורה שלו ברמה העליונה. ההגדרה window.top.location תגרום לחריגה, ולחיצה על קישור עם target="_top" לא תשפיע.
  • תכונות שמופעלות באופן אוטומטי (רכיבי טופס עם מיקוד אוטומטי, סרטונים שמופעלים באופן אוטומטי וכו') נחסמות.
  • אי אפשר לנעול את מצביע העכבר.
  • המערכת מתעלמת מהמאפיין seamless ב-iframes שהמסמך הממוסגר מכיל.

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

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

  • allow-forms מאפשר שליחת טפסים.
  • ‫allow-popups מאפשר (הפתעה!) חלונות קופצים.
  • allow-pointer-lock מאפשרת (הפתעה!) נעילת מצביע.
  • allow-same-origin מאפשר למסמך לשמור על המקור שלו. דפים שנטענים מ-https://example.com/ ישמרו על הגישה לנתונים של המקור הזה.
  • allow-scripts מאפשרת הרצה של JavaScript, וגם מאפשרת לתכונות לפעול באופן אוטומטי (כי קל להטמיע אותן באמצעות JavaScript).
  • allow-top-navigation מאפשר למסמך לצאת מהמסגרת על ידי ניווט בחלון ברמה העליונה.

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

  • צריך להוסיף את allow-scripts, כי הדף שנטען ב-frame מריץ קוד JavaScript כדי לטפל באינטראקציה של המשתמש.
  • צריך להשתמש ב-allow-popups כי הלחצן מקפיץ טופס ציוץ בחלון חדש.
  • חובה להשתמש ב-allow-forms, כי צריך להיות אפשר לשלוח את הטופס של הציוץ.
  • allow-same-origin נדרש, כי אחרת לא תהיה גישה לקובצי ה-Cookie של twitter.com, והמשתמש לא יוכל להתחבר כדי לפרסם את הטופס.

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

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

הפרדת הרשאות

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

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

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

הרצה בטוחה בארגז חול של eval()

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

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

<!-- frame.html -->
<!DOCTYPE html>
<html>
    <head>
    <title>Evalbox's Frame</title>
    <script>
        window.addEventListener('message', function (e) {
        var mainWindow = e.source;
        var result = '';
        try {
            result = eval(e.data);
        } catch (e) {
            result = 'eval() threw an exception.';
        }
        mainWindow.postMessage(result, event.origin);
        });
    </script>
    </head>
</html>

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

בגורם המטפל, אנחנו מקבלים את המאפיין source של האירוע, שהוא החלון הראשי. נשתמש בזה כדי לשלוח את התוצאה של העבודה הקשה שלנו בחזרה כשהיא תסתיים. אחר כך אנחנו נעשה את העבודה הקשה ונעביר את הנתונים שקיבלנו אל eval(). הקריאה הזו נעטפה בבלוק try, כי פעולות אסורות בתוך iframe בארגז חול (sandbox) יוצרות לעיתים קרובות חריגות DOM. אנחנו נתפוס אותן ונדווח במקום זאת על הודעת שגיאה ידידותית. בסופו של דבר, אנחנו מפרסמים את התוצאה בחלון הראשי. זה די פשוט.

גם ההורה פשוט באותה מידה. ניצור ממשק משתמש קטן עם textarea לקוד ו-button להרצה, ונייבא את frame.html באמצעות iframe בארגז חול, שמאפשר רק הרצת סקריפטים:

<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
        id='sandboxed'
        src='frame.html'></iframe>

עכשיו נחבר את הדברים כדי להפעיל את הניסוי. קודם אנחנו מאזינים לתשובות מה-iframe וalert() אותן למשתמשים שלנו. סביר להניח שאפליקציה אמיתית תעשה משהו פחות מעצבן:

window.addEventListener('message',
    function (e) {
        // Sandboxed iframes which lack the 'allow-same-origin'
        // header have "null" rather than a valid origin. This means you still
        // have to be careful about accepting data via the messaging API you
        // create. Check that source, and validate those inputs!
        var frame = document.getElementById('sandboxed');
        if (e.origin === "null" &amp;&amp; e.source === frame.contentWindow)
        alert('Result: ' + e.data);
    });

בשלב הבא, נחבר גורם מטפל באירועים לקליקים על button. כשהמשתמש לוחץ, אנחנו לוקחים את התוכן הנוכחי של textarea ומעבירים אותו ל-frame להפעלה:

function evaluate() {
    var frame = document.getElementById('sandboxed');
    var code = document.getElementById('code').value;
    // Note that we're sending the message to "*", rather than some specific
    // origin. Sandboxed iframes which lack the 'allow-same-origin' header
    // don't have an origin which you can target: you'll have to send to any
    // origin, which might alow some esoteric attacks. Validate your output!
    frame.contentWindow.postMessage(code, '*');
}

document.getElementById('safe').addEventListener('click', evaluate);

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

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

עם זאת, חשוב לשים לב כשמטפלים בתוכן ממוסגר שמגיע מאותו מקור כמו ההורה. אם דף ב-https://example.com/ ממסגר דף אחר באותו מקור באמצעות ארגז חול שכולל את הדגלים allow-same-origin ו-allow-scripts, הדף הממוסגר יכול להגיע אל ההורה ולהסיר את מאפיין ארגז החול לחלוטין.

משחקים בארגז החול

ארגז החול זמין עכשיו במגוון דפדפנים: Firefox 17 ואילך, IE10 ואילך ו-Chrome בזמן כתיבת המאמר (ב-caniuse, כמובן, יש טבלת תמיכה עדכנית). הוספת המאפיין sandbox לרכיבי iframes מאפשרת להעניק הרשאות מסוימות לתוכן שמוצג בהם, רק את ההרשאות שנדרשות כדי שהתוכן יפעל כמו שצריך. כך אפשר לצמצם את הסיכון שקשור להכללת תוכן של צד שלישי, מעבר למה שכבר אפשר לעשות באמצעות מדיניות אבטחת תוכן.

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

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

קריאה נוספת

  • "Privilege Separation in HTML5 Applications" הוא מאמר מעניין שעוסק בתכנון של מסגרת קטנה, וביישום שלה בשלוש אפליקציות HTML5 קיימות.

  • ארגז החול יכול להיות גמיש עוד יותר בשילוב עם שני מאפייני iframe חדשים אחרים: srcdoc ו-seamless. הראשון מאפשר לאכלס מסגרת בתוכן בלי התקורה של בקשת HTTP, והשני מאפשר לסגנון לזרום לתוכן הממוסגר. נכון לעכשיו, התמיכה של הדפדפנים בשתי האפשרויות האלה די גרועה (Chrome ו-WebKit nightlies), אבל בעתיד זה יהיה שילוב מעניין. לדוגמה, אפשר להפעיל ארגז חול לתגובות על מאמר באמצעות הקוד הבא:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>