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

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

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

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

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

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

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

אמון, אבל אימות

הלחצן 'ציוץ' של Twitter הוא דוגמה מצוינת לפונקציונליות שאפשר להטמיע באתר בצורה בטוחה יותר באמצעות ארגז חול. ב-Twitter אפשר להטמיע את הלחצן באמצעות iframe באמצעות הקוד הבא:

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

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

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

<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 עם מאפיין ארגז חול ריק, המסמך במסגרת יועבר לארגז חול באופן מלא, ויהיה כפוף להגבלות הבאות:

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

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

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

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

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

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

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

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

הפרדת הרשאות

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

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

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

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

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

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

משחקים ב-Sandbox

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

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

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

מקורות מידע נוספים

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

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

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