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

תמיכה בדפדפן

  • 52
  • 79
  • 52
  • 15.4

מקור

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

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

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

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

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

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

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

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

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

מבנה מחמיר של 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 ל "מחמיר" ולכן למאובטח:

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

שימוש במדיניות CSP מחמירה

כדי לאמץ מדיניות CSP מחמירה, צריך:

  1. צריך להחליט אם האפליקציה צריכה להגדיר מדיניות CSP שמבוססת על חד-פעמי (hash) או חד-פעמית (hash).
  2. מעתיקים את ה-CSP מהקטע Strict CSP ומגדירים אותו ככותרת תגובה באפליקציה.
  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 על סמך גיבוב (hash)

ב-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).
  • כותרת או תג HTML <meta>. בפיתוח מקומי, קל יותר לשנות את ה-CSP באמצעות תג <meta> ולראות במהירות איך הוא משפיע על האתר. אבל:
    • בהמשך, כשפורסים את ה-CSP בסביבת הייצור, מומלץ להגדיר אותה ככותרת HTTP.
    • אם אתם רוצים להגדיר את ה-CSP במצב דוח בלבד, עליכם להגדיר אותה ככותרת, כי המטא תגים של CSP לא תומכים במצב דוחות בלבד.

אפשרות א': CSP שמבוסס על Nonce

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

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

יצירת צופן חד-פעמי (nonce) עבור CSP

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

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

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

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

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

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

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

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

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

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

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

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

תמיכה בדפדפן

  • 52
  • 79
  • 52
  • 15.4

מקור

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

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

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

אחרי שמוודאים שה-CSP לא חוסם סקריפטים לגיטימיים בסביבת הפיתוח המקומית, תוכלו לפרוס את ה-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 שבו אתם משתמשים (nonces, גיבובים, עם או בלי '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: בלגן מוצלח בין הקשחה לצמצום הפגיעה.

קריאה נוספת