צמצום של סקריפטים חוצי-אתרים (XSS) באמצעות מדיניות Content Security Policy מחמירה (CSP)

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

פרצת אבטחה XSS‏ (cross-site scripting), היכולת להחדיר סקריפטים זדוניים לאפליקציית אינטרנט, היא אחת מנקודות החולשה הגדולות ביותר באבטחת אינטרנט כבר יותר מעשור.

‫Content Security Policy‏ (CSP) היא שכבת אבטחה נוספת שעוזרת לצמצם את הסיכון למתקפות XSS. כדי להגדיר CSP, מוסיפים את הכותרת Content-Security-Policy HTTP לדף אינטרנט ומגדירים ערכים שקובעים אילו משאבים סוכן המשתמש יכול לטעון בדף הזה.

בדף הזה מוסבר איך להשתמש ב-CSP שמבוסס על גיבובים או על צפנים חד-פעמיים כדי לצמצם את הסיכון ל-XSS, במקום להשתמש ב-CSP שמבוסס על רשימת היתרים של מארחים, שלרוב משאיר את הדף חשוף ל-XSS כי אפשר לעקוף אותו ברוב ההגדרות.

מונח מפתח: nonce הוא מספר אקראי שמשתמשים בו רק פעם אחת כדי לסמן תג <script> כמהימן.

מונח מפתח: פונקציית גיבוב (hash) היא פונקציה מתמטית שממירה ערך קלט לערך מספרי דחוס שנקרא גיבוב (hash). אפשר להשתמש בגיבוב (לדוגמה, SHA-256) כדי לסמן תג <script> מוטבע כמהימן.

מדיניות אבטחת תוכן שמבוססת על ערכי Nonce או גיבוב (hash) נקראת בדרך כלל מדיניות CSP מחמירה. כשאפליקציה משתמשת ב-CSP מחמיר, תוקפים שמוצאים פגמים בהזרקת HTML בדרך כלל לא יכולים להשתמש בהם כדי לאלץ את הדפדפן להריץ סקריפטים זדוניים במסמך פגיע. הסיבה לכך היא ש-CSP מחמיר מאפשר רק סקריפטים מגובבים או סקריפטים עם ערך ה-nonce הנכון שנוצר בשרת, כך שתוקפים לא יכולים להריץ את הסקריפט בלי לדעת את ה-nonce הנכון לתגובה נתונה.

למה כדאי להשתמש ב-CSP מחמירה?

אם באתר שלכם כבר יש CSP שנראה כמו script-src www.googleapis.com, כנראה שהוא לא יעיל נגד סקריפטים חוצי-אתרים. סוג כזה של CSP נקרא CSP של רשימת היתרים. הם דורשים הרבה התאמה אישית, ותוקפים יכולים לעקוף אותם.

מדיניות CSP מחמירה שמבוססת על ערכי גיבוב (hash) או ערכים חד-פעמיים (nonce) קריפטוגרפיים מונעת את הבעיות האלה.

מבנה CSP מחמיר

מדיניות אבטחת תוכן בסיסית מחמירה משתמשת באחת מכותרות התגובה הבאות של HTTP:

CSP מחמיר שמבוסס על מספר חד-פעמי

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
איך פועל CSP מחמיר שמבוסס על מספר חד-פעמי.

CSP קפדני שמבוסס על גיבוב (hash)

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

המאפיינים הבאים הופכים את ה-CSP ל "מחמיר" ולכן למאובטח:

  • היא משתמשת בערכי Nonce‏ 'nonce-{RANDOM}' או בערכי גיבוב (hash)‏ 'sha256-{HASHED_INLINE_SCRIPT}' כדי לציין באילו תגי <script> מפתח האתר בוטח להפעלה בדפדפן של המשתמש.
  • היא מגדירה את 'strict-dynamic' כדי להפחית את המאמץ הנדרש לפריסת CSP שמבוסס על מספר חד-פעמי או על גיבוב (hash), על ידי מתן הרשאה אוטומטית להפעלת סקריפטים שנוצרו על ידי סקריפט מהימן. הפעולה הזו גם מאפשרת להשתמש ברוב הספריות והווידג'טים של JavaScript של צד שלישי.
  • הוא לא מבוסס על רשימות היתרים של כתובות URL, ולכן הוא לא סובל מעקיפות נפוצות של CSP.
  • הוא חוסם סקריפטים מוטבעים לא מהימנים, כמו גורמים מטפלים באירועים מוטבעים או מזהי URI מסוג javascript:.
  • היא מגבילה את object-src כדי להשבית פלאגינים מסוכנים כמו Flash.
  • היא מגבילה את base-uri כדי לחסום הזרקה של תגי <base>. כך תוקפים לא יכולים לשנות את המיקומים של סקריפטים שנטענים מכתובות URL יחסיות.

אימוץ CSP מחמיר

כדי להשתמש ב-CSP מחמיר, צריך:

  1. מחליטים אם האפליקציה צריכה להגדיר CSP שמבוסס על גיבוב (hash) או על צופן חד-פעמי.
  2. מעתיקים את ה-CSP מהקטע מבנה CSP מחמיר ומגדירים אותו ככותרת תגובה באפליקציה.
  3. משנים את המבנה של תבניות HTML וקוד בצד הלקוח כדי להסיר דפוסים שלא תואמים ל-CSP.
  4. פורסים את ה-CSP.

אתם יכולים להשתמש בבדיקת השיטות המומלצות של Lighthouse (גרסה 7.3.0 ואילך עם הדגל --preset=experimental) לאורך התהליך הזה כדי לבדוק אם באתר שלכם יש CSP, ואם הוא מחמיר מספיק כדי להיות יעיל נגד XSS.

אזהרה בדוח Lighthouse על כך שלא נמצאה מדיניות CSP במצב אכיפה.
אם באתר שלכם אין CSP, האזהרה הזו תוצג ב-Lighthouse.

שלב 1: מחליטים אם צריך CSP שמבוסס על צופן חד-פעמי או על גיבוב (hash)

כך פועלים שני הסוגים של CSP מחמיר:

‫CSP מבוסס-nonce

ב-CSP שמבוסס על צופן חד-פעמי, יוצרים מספר אקראי בזמן הריצה, כוללים אותו ב-CSP ומשייכים אותו לכל תג סקריפט בדף. תוקף לא יכול לכלול או להריץ סקריפט זדוני בדף, כי הוא צריך לנחש את המספר האקראי הנכון של הסקריפט. זה עובד רק אם אי אפשר לנחש את המספר, והוא נוצר מחדש בזמן הריצה לכל תשובה.

משתמשים ב-CSP שמבוסס על מספר חד-פעמי לדפי HTML שמוצגים בשרת. בדפים האלה, אתם יכולים ליצור מספר אקראי חדש לכל תגובה.

‫CSP מבוסס-גיבוב (hash)

ב-CSP שמבוסס על גיבוב, הגיבוב של כל תג סקריפט מוטבע מתווסף ל-CSP. לכל סקריפט יש גיבוב (hash) שונה. תוקף לא יכול לכלול או להריץ סקריפט זדוני בדף, כי ה-hash של הסקריפט הזה צריך להיות ב-CSP כדי שהוא יפעל.

השתמשו ב-CSP מבוסס-גיבוב (hash) לדפי HTML שמוצגים באופן סטטי, או לדפים שצריך לשמור במטמון. לדוגמה, אפשר להשתמש ב-CSP שמבוסס על גיבוב (hash) באפליקציות אינטרנט של דף יחיד, שנבנו באמצעות מסגרות כמו Angular,‏ React או אחרות, שמוצגות באופן סטטי בלי עיבוד בצד השרת.

שלב 2: מגדירים מדיניות CSP מחמירה ומכינים את הסקריפטים

כשמגדירים CSP, יש כמה אפשרויות:

  • מצב דיווח בלבד (Content-Security-Policy-Report-Only) או מצב אכיפה (Content-Security-Policy). במצב דיווח בלבד, ה-CSP עדיין לא יחסום משאבים, כך ששום דבר באתר לא יפסיק לפעול, אבל תוכלו לראות שגיאות ולקבל דוחות על כל מה שהיה נחסם. באופן מקומי, כשמגדירים את ה-CSP, זה לא ממש משנה, כי שני המצבים מציגים את השגיאות במסוף הדפדפן. למעשה, מצב אכיפה יכול לעזור לכם למצוא משאבים שהטיוטה של ה-CSP חוסמת, כי חסימת משאב יכולה לגרום לדף להיראות לא תקין. מצב דיווח בלבד שימושי יותר בשלב מאוחר יותר בתהליך (ראו שלב 5).
  • כותרת או תג HTML‏ <meta>. לפיתוח מקומי, תג <meta> יכול להיות נוח יותר לשינוי ה-CSP ולראות במהירות איך הוא משפיע על האתר. עם זאת:
    • בהמשך, כשפורסים את ה-CSP בסביבת הייצור, מומלץ להגדיר אותו ככותרת HTTP.
    • אם אתם רוצים להגדיר את ה-CSP במצב דוחות בלבד, תצטרכו להגדיר אותו ככותרת, כי מטא תגים של CSP לא תומכים במצב דוחות בלבד.

אפשרות א': CSP מבוסס-nonce

מגדירים את כותרת התגובה הבאה של Content-Security-Policy HTTP באפליקציה:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

יצירת מספר חד-פעמי ל-CSP

מספר חד-פעמי הוא מספר אקראי שמשמש רק פעם אחת בכל טעינת דף. מדיניות CSP שמבוססת על מספר חד-פעמי יכולה לצמצם את הסיכון ל-XSS רק אם התוקפים לא יכולים לנחש את ערך המספר החד-פעמי. ה-nonce של ה-CSP חייב להיות:

  • ערך אקראי חזק מבחינה קריפטוגרפית (באופן אידיאלי באורך של 128 ביט או יותר)
  • נוצרת מחדש בכל תשובה
  • בקידוד Base64

הנה כמה דוגמאות להוספת צופן חד-פעמי (nonce) של CSP במסגרות בצד השרת:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

מוסיפים מאפיין nonce לרכיבי <script>

ב-CSP שמבוסס על צופן חד-פעמי, כל רכיב <script> חייב לכלול מאפיין nonce שתואם לערך הצופן החד-פעמי האקראי שצוין בכותרת ה-CSP. לכל הסקריפטים יכול להיות אותו nonce. השלב הראשון הוא להוסיף את המאפיינים האלה לכל הסקריפטים כדי שמדיניות ה-CSP תאפשר אותם.

אפשרות ב': כותרת תגובה של CSP שמבוססת על גיבוב (hash)

מגדירים את כותרת התגובה הבאה של Content-Security-Policy HTTP באפליקציה:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

אם יש כמה סקריפטים מוטבעים, התחביר הוא: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

טעינה דינמית של סקריפטים ממקורות שונים

אתם יכולים לטעון סקריפטים של צד שלישי באופן דינמי באמצעות סקריפט מוטבע.

דוגמה להטמעה של סקריפטים.
מותר על ידי CSP
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
כדי לאפשר לסקריפט הזה לפעול, צריך לחשב את ה-hash של הסקריפט המוטבע ולהוסיף אותו לכותרת התגובה של ה-CSP, במקום ה-placeholder {HASHED_INLINE_SCRIPT}. כדי לצמצם את כמות הגיבובים, אפשר למזג את כל הסקריפטים שמוטמעים בשורה לסקריפט אחד. כדי לראות איך זה עובד, אפשר לעיין בדוגמה הזו ובקוד שלה.
נחסם על ידי CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
מדיניות CSP חוסמת את הסקריפטים האלה כי הם לא נוספו באופן דינמי ואין להם מאפיין integrity שתואם למקור מותר.

שיקולים בטעינת סקריפטים

הסקריפט שמוטמע בדף מוסיף את s.async = false כדי לוודא ש-foo יופעל לפני bar, גם אם bar נטען קודם. בקטע הקוד הזה, s.async = false לא חוסם את המנתח בזמן שהסקריפטים נטענים, כי הסקריפטים נוספים באופן דינמי. המנתח מפסיק רק בזמן שהסקריפטים פועלים, כמו בסקריפטים של async. עם זאת, חשוב לזכור את הדברים הבאים לגבי קטע הקוד הזה:

  • יכול להיות שאחד מהסקריפטים או שניהם יופעלו לפני שהמסמך יסיים לרדת. אם אתם רוצים שהמסמך יהיה מוכן כשהסקריפטים מופעלים, המתינו לאירוע DOMContentLoaded לפני שאתם מוסיפים את הסקריפטים. אם זה גורם לבעיה בביצועים כי הסקריפטים לא מתחילים לרדת מספיק מוקדם, אפשר להשתמש בתגי טעינה מראש בשלב מוקדם יותר בדף.
  • defer = true לא עושה כלום. אם אתם צריכים את ההתנהגות הזו, הריצו את הסקריפט באופן ידני כשצריך.

שלב 3: משנים את המבנה של תבניות ה-HTML ושל הקוד בצד הלקוח

אפשר להשתמש בגורמים מטפלים באירועים בתוך השורה (כמו onclick="…", onerror="…") ובמזהי URI של JavaScript‏ (<a href="javascript:…">) כדי להריץ סקריפטים. כלומר, תוקף שמוצא באג XSS יכול להחדיר HTML מהסוג הזה ולהריץ JavaScript זדוני. מדיניות CSP שמבוססת על Nonce או על גיבוב (hash) אוסרת על שימוש בתגי עיצוב מהסוג הזה. אם האתר שלכם משתמש באחת מהתבניות האלה, תצטרכו לשנות אותן לחלופות בטוחות יותר.

אם הפעלתם את CSP בשלב הקודם, תוכלו לראות הפרות של CSP במסוף בכל פעם ש-CSP חוסם תבנית לא תואמת.

דוחות על הפרות של CSP ב-Chrome Developer Console.
שגיאות במסוף לגבי קוד חסימה.

ברוב המקרים, התיקון פשוט:

שכתוב של גורמים מטפלים באירועים מוטבעים

מותר על ידי CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
‫CSP מאפשר גורמים מטפלים באירועים שרשומים באמצעות JavaScript.
נחסם על ידי CSP
<span onclick="doThings();">A thing.</span>
מדיניות אבטחת התוכן (CSP) חוסמת גורמים מטפלים באירועים שמוטמעים בקוד.

‫Refactor javascript: URIs

מותר על ידי CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
‫CSP מאפשר גורמים מטפלים באירועים שרשומים באמצעות JavaScript.
נחסם על ידי CSP
<a href="javascript:linkClicked()">foo</a>
‫CSP חוסם מזהי URI של javascript:.

מסירים את eval() מה-JavaScript

אם האפליקציה משתמשת ב-eval() כדי להמיר סריאליזציות של מחרוזות JSON לאובייקטים של JS, צריך לשנות את המופעים האלה ל-JSON.parse(), שגם מהיר יותר.

אם אתם לא יכולים להסיר את כל השימושים ב-eval(), אתם עדיין יכולים להגדיר מדיניות CSP מחמירה שמבוססת על מספר חד-פעמי, אבל אתם צריכים להשתמש במילת המפתח 'unsafe-eval' של CSP, מה שהופך את המדיניות שלכם למעט פחות מאובטחת.

דוגמאות נוספות לארגון הקוד מחדש כזה אפשר למצוא ב-Codelab הזה של CSP מחמיר:

שלב 4 (אופציונלי): הוספת חלופות לתמיכה בגרסאות דפדפן ישנות

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

אם אתם צריכים לתמוך בגרסאות ישנות יותר של דפדפנים:

  • כדי להשתמש ב-strict-dynamic, צריך להוסיף את https: כחלופה לגרסאות קודמות של Safari. במקרה כזה:
    • כל הדפדפנים שתומכים ב-strict-dynamic מתעלמים מהחלופה https:, כך שזה לא יפחית את חוזק המדיניות.
    • בדפדפנים ישנים, סקריפטים ממקורות חיצוניים יכולים להיטען רק אם הם מגיעים ממקור HTTPS. היא פחות מאובטחת מ-CSP מחמיר, אבל עדיין מונעת כמה גורמים נפוצים ל-XSS, כמו הזרקות של מזהי URI מסוג javascript:.
  • כדי להבטיח תאימות עם גרסאות דפדפן ישנות מאוד (מלפני 4 שנים או יותר), אפשר להוסיף את unsafe-inline כחלופה. כל הדפדפנים העדכניים מתעלמים מ-unsafe-inline אם יש ערך חד-פעמי (nonce) או גיבוב (hash) של CSP.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

שלב 5: פורסים את ה-CSP

אחרי שמוודאים שמדיניות אבטחת התוכן לא חוסמת סקריפטים לגיטימיים בסביבת הפיתוח המקומית, אפשר לפרוס את מדיניות אבטחת התוכן בסביבת ה-staging ואז בסביבת הייצור:

  1. (אופציונלי) פורסים את ה-CSP במצב דיווח בלבד באמצעות הכותרת Content-Security-Policy-Report-Only. מצב דיווח בלבד שימושי לבדיקת שינוי תוכנה שעלול לגרום לכשל, כמו CSP חדש בסביבת הייצור, לפני שמתחילים לאכוף את ההגבלות של ה-CSP. במצב "דוחות בלבד", מדיניות ה-CSP לא משפיעה על התנהגות האפליקציה, אבל הדפדפן עדיין יוצר שגיאות במסוף ודוחות על הפרות כשהוא נתקל בדפוסים שלא תואמים למדיניות ה-CSP, כדי שתוכלו לראות מה היה משתבש אצל משתמשי הקצה. מידע נוסף זמין במאמר Reporting API.
  2. אחרי שמוודאים שה-CSP לא יגרום לתקלות באתר למשתמשי הקצה, פורסים את ה-CSP באמצעות כותרת התגובה Content-Security-Policy. מומלץ להגדיר את ה-CSP באמצעות כותרת HTTP בצד השרת, כי היא מאובטחת יותר מתג <meta>. אחרי שתסיימו את השלב הזה, מדיניות ה-CSP תתחיל להגן על האפליקציה מפני XSS.

מגבלות

מדיניות CSP מחמירה בדרך כלל מספקת שכבת אבטחה נוספת וחזקה שעוזרת לצמצם את הסיכון ל-XSS. ברוב המקרים, CSP מצמצמת את שטח המתקפה באופן משמעותי, על ידי דחיית דפוסים מסוכנים כמו מזהי URI מסוג javascript:. עם זאת, בהתאם לסוג ה-CSP שבו אתם משתמשים (צפנים חד-פעמיים, גיבובים, עם או בלי 'strict-dynamic'), יש מקרים שבהם ה-CSP לא מגן על האפליקציה שלכם כמו שצריך:

  • אם אתם מוסיפים nonce לסקריפט, אבל יש הזרקה ישירות לגוף או לפרמטר src של רכיב ה-<script> הזה.
  • אם יש הזרקות למיקומים של סקריפטים שנוצרו באופן דינמי (document.createElement('script')), כולל לפונקציות של ספריות שיוצרות צומתי DOM מסוג script על סמך הערכים של הארגומנטים שלהם. זה כולל כמה ממשקי API נפוצים כמו .html() של jQuery, וגם .get() ו-.post() ב-jQuery < 3.0.
  • אם יש הזרקות של תבניות באפליקציות AngularJS ישנות. תוקף שיכול להחדיר קוד לתבנית AngularJS יכול להשתמש בה כדי להריץ קוד JavaScript אקראי.
  • אם המדיניות מכילה את הערך 'unsafe-eval', הזרקות ל-eval(), ל-setTimeout() ולכמה ממשקי API אחרים שמשתמשים בהם לעיתים רחוקות.

מפתחים ומהנדסי אבטחה צריכים לשים לב במיוחד לדפוסים כאלה במהלך סקרי קוד וביקורות אבטחה. במאמר Content Security Policy: A Successful Mess Between Hardening and Mitigation מפורט מידע נוסף על המקרים האלה.

קריאה נוספת