המאמר מניח שאתם מפתחים שמכירים טכניקת XSS (קישור למאמר שלי על XSS בסיסי) וכן אתם יודעים מה זה CSP (קישור למאמר על CSP בסיסי). כדאי מאד לקרוא את שני המאמרים האלו כי לטעמי הבנה בהם היא חלק משמעותי מהידע הנדרש למפתחי ווב בימים אלו.
בכל המאמרים שהיו עד כה על CSP, הראנו דוגמאות איך חוסמים סקריפטים ממקורות בלתי מורשים או איך משתמשים ב-nonce או ב-hash עבור סקריפטים שרצים באינליין. בעוד שכדאי ורצוי להשתמש ביכולות האלו, הן לא חוסמות את הכל. Trusted Types היא פיצ׳ר שקיים מזה זמן בכרום ונכנס גם בפיירפוקס ויכול להיות מאד שימושי בכל הנוגע להגנה מ-XSS בעולם האמיתי.
הבעיה המרכזית ב-XSS היא שבעולם המודרני הוא יכול להגיע מכל מקום. למשל, בואו ונדמיין שבאמת השתמשתי ב-CSP כדי לחסום סקריפטים ממקורות ספציפיים והשתמשתי ב-hash לחתימת סקריפטים ממקורות חיצוניים. עדיין תוקף יכול לתקוף חבילה שאני משתמש בה ב-npm או חבילה שהחבילה שאני משתמש בה. הבעיה היא שיש כל כך הרבה חולשות בכל מיני מקומות שאי אפשר למצוא אותן.
בואו נדמיין לשניה שיש לנו אתר אינטרנט, הטמענו את כל ה-CSP האפשריים ואז מישהו בא, תוקף את אחת הספריות שאנו משתמשים בהן והיא מזריקה אלינו את ה-payload הזה:
<!DOCTYPE html>
<html>
<head>
<title>Some page</title>
</head>
<body>
<h1>Trusted Types for DOM Manipulation</h1>
<div id="content"></div>
</body>
<script>
// some other code of the library
let div = document.getElementById("content");
let data = "<img src='not-there.jpg' onerror='window.location.href = `https://evil.com`'>";
div.innerHTML = data;
</script>
</html>
גם אם השתמשתי ב-nonce, זה לא יעזור לי. גם אם אני בולק inline, אם המודול הנגוע נמצא בתוך סקריפט אחר (פה שמתי אותו גלוי לשם הדוגמה) זה לא יעזור לי. הבעיה המרכזית פה היא innerHTML. בגדול, אם הייתי יכול לאסור על innerHTML באתר שלי, זה היה מושלם. וכמובן שגם קללות אחרות של implied eval.
אז עם CSP Trusted Types זה בהחלט אפשרי. אני יכול, היכן שאני רוצה, לאסור על innerHTML או להפעיל אותו בתנאים מסוימים.
דוגמה נאיבית ופשוטה
נתחיל עם האפשרות הכי נאיבית. לאסור על innerHTML לחלוטין. לא מיניה ולא מקצתיה כפי שנאמר במקורותינו. אני בעצם מכניס את מדיניות ה-CSP של require-trusted-types-for 'script'. בדוגמה שלי אני אכניס את זה כמטא תגית.
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script';">
מה שיקרה אם אני אריץ את הקוד להלן זו שגיאת CSP שאומרת: חבוב! innerHTML? צא בחוץ!
זה יעבוד גם עם כל מה שלוקח מידע וממיר אותו לקוד שרץ. וזה נפלא.
לצאת מה-Hello World
כלומר נפלא בדוגמאות Hello World. בפועל, בעולם האמיתי אנחנו שמים את זה ומגלים שכל האפליקציה שבורה. למה? כי באמת יש כל מיני מודולים וספריות שנעזרות ב-innerHTML באופן הכי לגיטימי שאפשר לפעולות שונות. סגרתי להן את innerHTML? סגרתי אותן. בעוד שבתיאוריה זה אחלה, בפועל זה בעייתי. מה אם אני יכול להגדיר מדיניות כלומר – אפשר להשתמש ב-innerHTML ועוזריו אבל בכל פעם שמשתמשים בהם, צריך לעשות סניטציה (שם של הליך שבמסגרתו מסירים את כל הקוד הבעייתי שיכול לרוץ). כך זה לא יחסום את הספריות/קוד לגאסי שאולי עושות שימוש ב-innerHTML כדי ליצור אלמנטים שונים או לצרכים אחרים.
אני יכול, באמצעות ג׳אווהסקריפט, להגדיר מדיניות ברירת מחדל (רק פעם אחת כמובן, אז את ההגדרה מומלץ לעשות ממש בתחילת הדף). למשל – הגדרה שכל innerHTML יעבור סניטציה עם dompurify. אני עושה את זה באמצעות trustedTypes.createPolicy – מתודה שקבלת שני פרמטרים – סוג, default במקרה שלנו ופונקציה שדרכה כל יצירת HTML תעבור.
למשל:
if (window.trustedTypes && trustedTypes.createPolicy) {
trustedTypes.createPolicy('default', {
createHTML: string => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
});
}
בתחילה אני עושה בדיקה שהפיצ׳ר trustedTypes קיים. הוא לא קיים בדפדפנים ישנים אז מומלץ לבדוק. ואז אני פשוט מגדיר שכל יצירת HTML באמצעות סקריפט תעבור ב-DOMPurify.sanitize.
הנה הדוגמה המלאה שמומלץ להוריד ולשחק איתה. זה באמת קל.
<!DOCTYPE html>
<html>
<head>
<title>Some page</title>
<meta
http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script';"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.7/purify.min.js"></script>
<script>
if (window.trustedTypes && trustedTypes.createPolicy) {
trustedTypes.createPolicy('default', {
createHTML: string => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
});
}
</script>
</head>
<body>
<h1>Trusted Types for DOM Manipulation</h1>
<div id="content"></div>
</body>
<script>
// some other code of the library
let div = document.getElementById("content");
let data =
"<img src='not-there.jpg' onerror='window.location.href = `https://evil.com`'>";
div.innerHTML = data;
</script>
</html>
אם תריצו את הדוגמה הזו תראו שאין שגיאות בקונסול (חוץ מזה שהתמונה לא קיימת) אבל גם הקוד הזדוני לא רץ כי ספרית הסניטציה ניקתה את ה-img.
הגדרת מדיניות בקוד לגאסי
בחיים האמיתיים, אם נעשה את זה, יש דברים שעדיין יישברו כי – תאמינו או לא – יש ליטרלי ספריות ש-XSS הוא חלק מאיך שהן פועלות. אם מדובר בספריות צד שלישי – אז במקרה הזה מוטב להסיר אותן/למצוא להן אלטרנטיבה כי מדובר בחולשת אבטחה משמעותית. אם מדובר בקוד שלכם, יש אפשרות להגדיר ספציפית מדיניות אבטחה אחרת רק להן. איך?
יוצרים מדיניות אבטחה נוספת, אבל עם שם שהוא לא default, מכניסים את השם של המדיניות ב-CSP ואז משתמשים בה בקוד הלגיטימי שלנו.
הכי טוב זה כמובן להדגים. בואו נדמיין שניה קוד שיוצר img לגיטימי שיש בו alert. נניח שאני רוצה את ההתנהגות הזו, משתוקק לה, בוער לה. אז במקום לוותר על מדיניות ה-CSP, אני אגדיר שעבור הקוד הזה הבדיקה תהיה מחמירה פחות (או שלא תהיה בדיקה בכלל).
מה שאני צריך לעשות זה ליצור trusted type בדיוק כמו בדוגמה הקודמת ולקרוא לה בשם במקום default. למשל legacySanitize. השלב הבא הוא להכניס אותה ל-trusted-type ב-CSP.
השלב האחרון והחשוב הוא לשנות את קוד הלגאסי על מנת להשתמש ב-createHTML בכל מופע של innerHTML. למשל:
div.innerHTML = legacySanitizer.createHTML(data);
זה השלב שהוא קצת מסובך אז אתן לקוד לדבר והדגשתי את המופע שרציתי.
<!DOCTYPE html>
<html>
<head>
<title>Trusted Types Example</title>
<meta
http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script'; trusted-types legacySanitize"
/>
<script>
const legacySanitizer = trustedTypes.createPolicy("legacySanitize", {
createHTML: (htmlString) => {
const crappySanitizedHtml = htmlString.replace(
/<script.*?>.*?<\/script>/gi,
""
);
return crappySanitizedHtml;
},
});
</script>
<script>
function legacyRender() {
let div = document.getElementById("contentDiv");
data =
'<img src="https://picsum.photos/200/300" onClick="alert(`Legitimate action`)" />';
div.innerHTML = legacySanitizer.createHTML(data);
}
</script>
</head>
<body>
<h1>Trusted Types Demo</h1>
<div id="contentDiv"></div>
<button onclick="legacyRender()">legacyRender</button>
</body>
</html>
ואם תנסו את הדוגמה הזו, אתם תראו שזה עובד יופי.
יש עוד על מה לדבר ולדון בנושא. אני אפרסם מאמר המשך המסביר על שימוש גם בריאקט/אנגולר (בגדול די דומה ופשוט יותר). מדובר בטכניקת הגנה שממש תפסה תאוצה, במיוחד אחרי שפיירפוקס הודיעו החודש שהם מטמיעים את זה (בכרום זה קיים כבר ממאי 2020). כרגע כן חשוב להקפיד לבדוק אם זה קיים כיוון שגם ספארי קללת המין האנושי לא תומך בזה. עדיין.
8 תגובות
יצא לך אולי לעשות בנצ׳מרק על הביצועים ?
כמה והאם סניטציה תשפיע על ביצועים ?
לא ראיתי השפעה אבל… זה אחלה רעיון לפוסט! אני אעשה את הבדיקה ואפרסם.
תודה אבי! 🙂
תוכל לפרט יותר על סניטציה? אני לא מתכנת פרונט קלאסי ושמעתי את המושג אבל אשמח אם תוכל להקדיש לו פוסט משל עצמו.
בשמחה ואני אשתדל בשבוע הבא 🙂
תודה!
אבל לא הבנתי משהו, אם יש לי פאקג' נגוע איפשהו בעץ התלויות, הוא לא צריך להשתמש ב-innerHTML כדי להזריק קוד זדוני, הוא יכול להריץ את את הקוד הזדוני ישירות ולעשות מה שבא לו.
לאו דווקא יוצר החבילה אלא פשוט חולשה בחבילה שתוקף זדוני אחר יכול להשתמש בה.
אם אפשר לתקן את המילים המחוברות 'מאדשימושי'
תודה רבה! תוקן!