יישום hash על משאבי inline עם CSP

הסבר על שימוש ב-hash ב CSP כדי להמנע מ unsafe-inline.

מאמר זה הוא מאמר המשך למאמר המסביר על SRI בעולם המודרני ועל חתימות. מומלץ לקרוא אותו קודם.

אחד מהפיצ׳רים הכי חזקים לאבטחת אתרים/אפליקציות שרצות בווב הוא CSP, ראשי תבות של Content Security Policy. זה מוטמע ברמת השרת ומשפיע מאד על צד הלקוח והיכולת שלו לטעון משאבים שונים. כתבתי מבוא ל-CSP שמאוד רלוונטי למרות שנכתב לפני כמה שנים – אם אתם לא מכירים כלל את CSP – מומלץ לקרוא את הפוסט הקצר שיעשה לכם סדר כי אם לא יודעים מה זה CSP אז יהיה קשה להבין איך משתמשים ב-hash כחלק ממנו 😇

בשנים האחרונות יש שינויים ותוספות ב-CSP שיש לו מדיניות מסודרת שמתוארת ב-W3C שהוא זה שמאחורי הסטנדרט הזה. השלב השלישי של CSP, שיש לו שינויים רבים ותוספות חשובות, נמצא בטיוטה בזמן כתיבת הפוסט.

פיצ׳ר משמעותי וחשוב של CSP הוא hash אבל בניגוד ל-SRI שעליו כתבנו במאמר הקודם הוא נוגע לקבצי inline בלבד.

בגדול, CSP מאפשר לנו לטעון משאבים שאנחנו רוצים. למשל, אני יכול לבקש ממנו לטעון אך ורק קבצי ג׳אווהסקריפט מ my.cdn.example.com. אם אני אנסה לטעון קובץ ג׳אווהסקריפט אחר ולהריץ אותו, אני אכשל. זה נועד למנוע מתוקפים לטעון סקריפטי תקיפה. למשל, אם תוקף משתיל קובץ/קוד באתר שקורא ל evil.com/evil-attack.js, הוא לא ייטען. טוב, את זה אנחנו מכירים, אבל אנחנו יכולים להשתמש בו גם כדי למנוע טעינה של קבצי inline שלא נבדקו קודם.

בואו ונדגים: נניח שיש לנו דף אינטרנט פשוט שיש בו inline js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1 id="header">Hello, World!</h1>
    <script>
        document.addEventListener('DOMContentLoaded', (event) => {
            document.getElementById('header').textContent = 'DOM Manipulated!';
        });
    </script>
</body>
</html>

זה דבר שיכול לקרות וגם קורה לא מעט פעמים במיוחד בפריימוורקים החדשים. אם אני רוצה להכניס CSP – אני בבעיה. למה? כי CSP אוסר כברירת מחדל על inline. אם אני רוצה לאפשר את זה אני צריך להכניס מדיניות של unsafe inline.

<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';">

זה כמובן רע מאד – כי זה בעצם מאיין את ה-CSP ומאפשר לתוקפים להריץ ג׳אווהסקריפט אינלייני בלי שום בעיה. אבל גם אם תתעלמו מנושא האבטחה – תראו לי מומחה אבטחה אחד שיהיה מאושר מלהכניס unsafe-inline לאיזושהי קטגוריה ב-CSP שלו 🙂 בגדול זה פתרון שהוא לא קביל.

אז אם אין unsafe-inline? מה עושים?

יש כמה דרכים להתמודד עם inline ואני אציג את הראשונה פה ואת הבאה בפוסט הבא – הראשונה היא חישוב hash לסקריפט inline והכנסתו ל-CSP. כן! ממש ככה.

בואו ונדגים איך זה עובד עם דוגמת ה-hello world שלנו. בגדול – צריך לחשב את ה-hash ולשים אותו ב-base64. איך? עם הפקודה:

echo "        document.addEventListener('DOMContentLoaded', (event) => {
            document.getElementById('header').textContent = 'DOM Manipulated!';
        });" | openssl sha256 -binary | openssl base64

הבעיה שאני עצל מדי לזה, אז אפשר לעשות משהו אחר. אני אכניס hash שהמצאתי

<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-ahlabahla=';">

וארפרש את הדף. ישר אני אקבל שגיאה – אבל, וזה אבל גדול – אני אקבל את ה-hash שהדפדפן מצפה לו! שזה מה שאני צריך לשים ב-CSP.

שתי שגיאות. הראשונה:
The source list for the Content Security Policy directive 'script-src' contains an invalid source: ''sha256-ahlabahla=''. It will be ignored.
השניה:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'sha256-ahlabahla='". Either the 'unsafe-inline' keyword, a hash ('sha256-ft0eAUjMFe4qVG8iGj6b/pNfUH7quaI++HXTUFmQ4E4='), or a nonce ('nonce-...') is required to enable inline execution.
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-ft0eAUjMFe4qVG8iGj6b/pNfUH7quaI++HXTUFmQ4E4=';">

וזה יעבוד מעולה. אם תוקף ינסה להכניס inline code כלשהו שאינו עונה על ה-hash הזה – אז אבוד לו. הוא לא יצליח להריץ אותו והתקפת ה-xss (או stored או reflected) תיכשל. הידד!

או כמעט הידד. יש כמה בעיות רציניות עם הגישה הזו. הראשונה היא שזה לא יעבוד אם יש לנו אירועים באלמנטים. למשל משהו כזה:

  <body>
    <h1 id="header">Hello, World!</h1>
    <button onclick="clickHandler();">Click here!</button>
    <script>
      function clickHandler() {
        document.addEventListener("DOMContentLoaded", (event) => {
          document.getElementById("header").textContent = "DOM Manipulated!";
        });
      }
    </script>
  </body>

אז ה-onclick לא יעבוד ונקבל שגיאת CSP. יש בתקן החדש של CSP פתרון לזה בדמות unsafe-hashes אבל לא כל הדפדפנים תומכים וגם אלו שתומכים לא תומכים מספיק טוב ומניסיון אם משתמשים בזה די אוכלים קש.

הבעיה המשמעותית האמיתית היא שזה לא מחזיק מים בעולם האמיתי. כלומר באתרים סטטיים בלבד שנבנים לבד – לגמרי עובד. אפשר גם לכתוב סקריפט בתהליך הבילד שעובר על כל ה-HTMLים עם ה-inline, אבל זה לא יתפוס. במיוחד אם משתמשים ב-CSS-in-JS או בדברים שיוצרים אינליין דינמי. זה לא יעבוד והאמת היא שכדאי להכיר איך hash עובד כדי לפסול אותו א-פריורית ברוב המקרים הרלוונטיים כאשר מתחילים לדבר על יישום CSP בסביבה מודרנית ולא לבזבז עליו את הזמן.

אז איך אנחנו כן עובדים עם CSP בלי hash? התשובה היא nonce ונושא המאמר הבא על CSP ו-nonce.

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

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