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

תמיכה בדפדפנים

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

מקור

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

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

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

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

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

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

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

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

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

מבנה מחמיר של CSP

מדיניות Content Security בסיסית וקפדנית משתמשת באחד מהכותרות הבאות של תגובת HTTP:

מדיניות CSP מחמירה ולא מבוססת

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

CSP קפדני מבוסס-גיבוב

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

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

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

מאמצים מדיניות CSP מחמירה

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

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

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

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

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

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

CSP מבוסס-nonce

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

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

CSP מבוסס-גיבוב

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

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

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

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

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

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

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

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

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

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}'.

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

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

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

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

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

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

שינוי מבנה של גורמים שמטפלים באירועים בקוד

מותרת על ידי 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 חוסם גורמים מטפלים באירועים בקוד.

שינוי מבנה של מזהי URI מסוג javascript:

מותרת על ידי 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 חוסמת את JavaScript: URI.

הסרת eval() מ-JavaScript

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

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

תוכלו למצוא את הדוגמאות האלה ועוד דוגמאות ארגון מחדש (Refactoring) מהסוג הזה ב-Codelab המחמיר של CSP:

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

תמיכה בדפדפן

  • Chrome: 52.
  • קצה: 79.
  • Firefox: 52.
  • Safari: 15.4.

מקור

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

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

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

אחרי שתאשרו שה-CSP לא חוסם סקריפטים חוקיים בסביבת הפיתוח המקומית, תוכלו לפרוס את ה-CSP בסביבת ייצור, ואז בסביבת הייצור:

  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 לא מגן על האפליקציה גם כן:

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

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

קריאה נוספת