כיום, כמעט בלתי אפשרי ליצור חוויה עשירה באינטרנט בלי להטמיע רכיבים ותוכן שאין לכם שליטה אמיתית עליהם. ווידג'טים של צד שלישי יכולים להגביר את המעורבות ולמלא תפקיד קריטי בחוויית המשתמש הכוללת, ולפעמים תוכן שנוצר על ידי משתמשים חשוב אפילו יותר מהתוכן המקורי של אתר. אין שום אפשרות להימנע משניהם, אבל שתי האפשרויות גורמות לסיכון שמשהו רע בגלל שגיאה באתר. כל ווידג'ט שמוטמע באתר – כל מודעה, כל ווידג'ט של רשת חברתית – הוא כיוון התקפה פוטנציאלי לגורמים עם כוונות זדוניות:
אפשר להשתמש ב-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 כדי לקשר את הציוץ לחשבון הנכון, והוא זקוק ליכולת לשלוח את טופס הציוץ. זהו בערך הסיפור כולו. המסגרת לא צריכה לטעון יישומי פלאגין, היא לא צריכה לנווט בחלון ברמה העליונה או לבצע פעולות פונקציונליות אחרות. מכיוון שהן לא נדרשות, נבטל אותן על ידי הכנסת התוכן של המסגרת לארגז חול.
הכלי לבדיקה בסביבה וירטואלית פועל על סמך רשימת היתרים. מתחילים בהסרת כל ההרשאות האפשריות, ולאחר מכן מפעילים מחדש יכולות ספציפיות על ידי הוספת דגלים ספציפיים להגדרות של ה-Sandbox. בווידג'ט של 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 עם מאפיין sandbox ריק, המסמך במסגרת יועבר לארגז חול (sandbox) באופן מלא, ויהיה כפוף להגבלות הבאות:
- JavaScript לא יפעל במסמך הממוסגר. המשמעות היא שהתכנים האלה לא כוללים רק JavaScript שנטען באופן מפורש באמצעות תגי סקריפט, אלא גם פונקציות טיפול באירועים בקוד ומחרוזות URL מסוג javascript: . המשמעות היא גם שהתוכן שמכילים תגי noscript יוצג בדיוק כאילו המשתמש השבית את הסקריפט בעצמו.
- המסמך הממוסגר נטען למקור ייחודי, כלומר כל הבדיקות של מקור זהה ייכשלו. מקורות ייחודיים לא תואמים למקורות אחרים אף פעם, אפילו לא לעצמם. בין היתר, המשמעות היא שלמסמך אין גישה לנתונים שמאוחסנים בקובצי cookie של כל מקור או במנגנוני אחסון אחרים (אחסון DOM, Indexed DB וכו').
- לא ניתן ליצור חלונות או תיבת דו-שיח חדשים במסמך הממוסגר (לדוגמה, באמצעות
window.open
אוtarget="_blank"
). - אי אפשר לשלוח טפסים.
- הפלאגינים לא ייטענו.
- אפשר לנווט רק במסמך המוסגר, ולא בהורה שלו ברמה העליונה.
ההגדרה
window.top.location
תגרום להשלכת חריגה, ולקליק על קישור עםtarget="_top"
לא תהיה השפעה. - תכונות שפועלות באופן אוטומטי (רכיבי טפסים שמתמקדים באופן אוטומטי, סרטונים שפועלים אוטומטית וכו') חסומים.
- לא ניתן לקבל את נעילת הסמן.
- המאפיין
seamless
מתעלם מ-iframes
שמכיל את המסמך הממוסגר.
זו אכיפה דרקונית נעימה, ומסמך שנטען ב-iframe
במצב ארגז חול מלא אכן לא מסכן במיוחד. כמובן, הוא גם לא יכול להפיק ערך רב: יכול להיות שאפשר לצאת מהארגז חול עם ארגז חול מלא לתוכן סטטי, אבל ברוב המקרים כדאי להשתחרר קצת.
מלבד יישומי פלאגין, אפשר לבטל כל אחת מההגבלות האלה על ידי הוספת דגל לערך של מאפיין ארגז החול. במסמכים בארגז חול אי אפשר להריץ אף פעם יישומי פלאגין, כי יישומי פלאגין הם קוד מקורי שלא נמצא בארגז חול, אבל כל שאר הדברים מותרים:
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
מוגדר, הווידג'ט מקבל רק את ההרשאות
שנדרשות, ויכולות כמו יישומי פלאגין, ניווט בחלק העליון ונעילת הסמן יישארו חסומות. הפחתנו את הסיכון להטמעת הווידג'ט, ללא השפעות שליליות.
כך כולם מרוויחים.
הפרדת הרשאות
ברור שזה מועיל להעביר תוכן של צד שלישי לארגז חול כדי להריץ את הקוד הלא מהימן שלו בסביבה עם הרשאות נמוכות. אבל מה לגבי הקוד שלכם? אתם סומכים על עצמכם, נכון? אז למה כדאי להשתמש בארגז חול?
אני רוצה להפוך את השאלה: אם הקוד לא צריך פלאגינים, למה לתת לו גישה לפלאגינים? במקרה הטוב, זוהי הרשאה שאתם אף פעם לא משתמשים בה, ובמקרה הרע היא נתיב פוטנציאלי לתוקפים כדי לקבל גישה לחשבון. לכל קוד יש באגים, וכמעט כל אפליקציה חשופה לניצול לרעה בדרך כזו או אחרת. כשמשתמשים בסביבת חול לקוד, גם אם תוקף מצליח לפרוץ לאפליקציה, לא תהיה לו גישה מלאה למקור שלה. הוא יוכל לבצע רק פעולות שהאפליקציה יכולה לבצע. עדיין לא טוב, אבל לא נורא כמו שיכול להיות.
כדי לצמצם את הסיכון עוד יותר, אפשר לפצל את האפליקציה לחלקים לוגיים ולהעביר כל חלק לארגז חול עם ההרשאות המינימליות האפשריות. הטכניקה הזו נפוצה מאוד בקוד מקורי: לדוגמה, Chrome מחולק לתהליך דפדפן עם הרשאות גבוהות שיש לו גישה לכונן הקשיח המקומי ויכול ליצור חיבורי רשת, ולתהליכי רינדור רבים עם הרשאות נמוכות שמבצעים את העבודה הקשה של ניתוח תוכן לא מהימן. הנגנים לא צריכים לגשת לדיסק, הדפדפן דואג לספק להם את כל המידע הדרוש כדי ליצור עיבוד (render) של דף. גם אם האקר חכם ימצא דרך לפגוע במעבד, הוא לא יוכל להתקדם הרבה, כי המעבד לא יכול לבצע פעולות מעניינות בעצמו: כל הגישה עם הרשאות גבוהות חייבת לעבור דרך התהליך של הדפדפן. כדי לגרום נזק, תוקפים יצטרכו למצוא כמה חורים בחלקים שונים של המערכת, מה שמקטין מאוד את הסיכון לפריצה מוצלחת.
הרצה בטוחה בארגז חול של 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" && 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, הדף המסגר יכול להגיע אל הדף ההורה ולהסיר את מאפיין ארגז החול לגמרי.
הפעלה בארגז החול
אפשר להשתמש בסביבת חול במגוון דפדפנים: Firefox מגרסה 17 ואילך, IE מגרסה 10 ואילך ו-Chrome נכון למועד כתיבת המאמר (ב-caniuse יש כמובן טבלת תמיכה עדכנית). החלת המאפיין sandbox
על iframes
שאתם כוללים מאפשרת לכם להקצות הרשאות מסוימות לתוכן שהן מציגות, רק את ההרשאות הנחוצות לתפקוד תקין של התוכן. כך תוכלו לצמצם את הסיכון שמשויך להכללת תוכן של צד שלישי, מעבר למה שכבר אפשר לעשות באמצעות מדיניות אבטחת התוכן.
בנוסף, שימוש בסביבת חול הוא שיטה יעילה לצמצום הסיכון שמתקפה חכמה תוכל לנצל חורים בקוד שלכם. אפליקציה מונוליתית מופרדת לקבוצת שירותים בארגז חול, שכל אחד מהם אחראי לחלק קטן של פונקציונליות עצמאית, ייאלצו לא רק לפגוע בתוכן של פריימים מסוימים אלא גם בבקר שלהם. זו משימה הרבה יותר קשה, במיוחד מפני שהבקר יכול להיות מצומצם מאוד. אתם יכולים להשקיע את המאמצים הקשורים לאבטחה בבדיקה של הקוד הזה, אם תבקשו מהדפדפן עזרה בשאר הקוד.
עם זאת, אי אפשר לומר ש-sandboxing הוא פתרון מלא לבעיית האבטחה באינטרנט. היא מספקת הגנה לעומק, ואם אין לכם שליטה על לקוחות המשתמשים, עדיין אי אפשר להסתמך על תמיכה בדפדפנים לכל המשתמשים (אם יש לכם שליטה על לקוחות המשתמשים – בסביבה ארגונית, למשל – יופי!). יום אחד… אבל בינתיים, ארגז החול הוא שכבת הגנה נוספת שמחזקת את ההגנות שלכם, אבל הוא לא הגנה מלאה שאפשר להסתמך עליה בלבד. עם זאת, השכבות מצוינות. אני מציעה להשתמש בזה.
מקורות מידע נוספים
Privilege Separation in HTML5 Applications הוא מאמר מעניין שמתאר את תכנון המסגרת הקטנה ואת היישום שלה בשלוש אפליקציות 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>