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

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

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

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

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

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

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

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

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

הפרדת הרשאות

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

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

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

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

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

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

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

ב-handler, אנחנו לוקחים את המאפיין source של האירוע, שהוא חלון ההורה. נשתמש בו כדי להחזיר את התוצאה של העבודה הקשה שלנו בסיום. לאחר מכן נבצע את העבודה הקשה ונעביר את הנתונים שקיבלנו במסגרת eval(). השיחה הזו נסגרה בבלוק של ניסוי, כי פעולות חסימה בארגז חול iframe יוצרות בדרך כלל חריגות 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);
    });

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

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

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

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

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

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

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

קריאה נוספת

  • "הפרדת הרשאות באפליקציות HTML5" הוא מאמר מעניין שעובד על תכנון של framework קטנה ועל היישום שלה בשלוש אפליקציות HTML5 קיימות.

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

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