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