הגנה מפני XSS עם Trusted Types

תכונה ב-CSP שמאפשרת מניעה כמעט הרמטית להתקפות XSS שכל מפתח ווב צריך להכיר וכדאי שיכיר.
תמונת תצוגה של מנעול על מחשב

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

פוסטים נוספים שכדאי לקרוא

רספברי פיי

הרצת גו על רספברי פיי

עולם הרספברי פיי והמייקרים ניתן לתפעול בכל שפה – לא רק פייתון או C – כאן אני מסביר על גו

גלילה לראש העמוד