Cross-site scripting (XSS), היכולת להחדיר סקריפטים זדוניים לאפליקציית אינטרנט, היא אחת מנקודות החולשה הגדולות ביותר באבטחת האינטרנט כבר יותר מעשור.
Content Security Policy (CSP) היא שכבת אבטחה נוספת שעוזרת לצמצם את הסיכון לפגיעות XSS. כדי להגדיר CSP, מוסיפים את כותרת ה-HTTP Content-Security-Policy
לדף אינטרנט ומגדירים ערכים שקובעים אילו משאבים סוכן המשתמש יכול לטעון לדף הזה.
בדף הזה מוסבר איך להשתמש ב-CSP שמבוסס על גיבובים או על גיבוב חד-פעמי (nonce) כדי לצמצם את הסיכון ל-XSS, במקום ב-CSP נפוצים שמבוססים על רשימת ההיתרים של המארח, שבדרך כלל משאירים את הדף חשוף ל-XSS כי אפשר לעקוף אותם ברוב ההגדרות.
מונח מפתח: nonce הוא מספר אקראי שמשמש רק פעם אחת, וניתן להשתמש בו כדי לסמן תג <script>
כמהימן.
מונח מפתח: פונקציית גיבוב היא פונקציה מתמטית שממירה ערך קלט לערך מספרי דחוס שנקרא גיבוב. אפשר להשתמש בגיבוב (למשל, SHA-256) כדי לסמן תג <script>
בקוד כמהימן.
מדיניות Content Security Policy שמבוססת על ערכים חד-פעמיים או על גיבוב נקראת לרוב CSP מחמירה. כשאפליקציה משתמשת ב-CSP מחמיר, תוקפים שמוצאים נקודות חולשה בהזרקת HTML בדרך כלל לא יכולים להשתמש בהן כדי לאלץ את הדפדפן להריץ סקריפטים זדוניים במסמך פגיע. הסיבה לכך היא ש-CSP מחמיר מאפשר רק סקריפטים מגובבים או סקריפטים עם ערך ה-nonce הנכון שנוצר בשרת, כך שמתקפות לא יכולות להריץ את הסקריפט בלי לדעת את ערך ה-nonce הנכון לתגובה נתונה.
למה כדאי להשתמש ב-CSP מחמיר?
אם באתר שלכם כבר יש CSP שנראה כמו script-src www.googleapis.com
, סביר להניח שהוא לא יעיל נגד התקפות חוצות-אתרים. סוג כזה של CSP נקרא CSP ברשימת ההיתרים. הן דורשות הרבה התאמה אישית, ואפשר לדלג עליהן.
כדי להימנע מהמלכודות האלה, צריך להשתמש ב-CSPs מחמירים שמבוססים על נתוני nonce קריפטוגרפיים או על גיבוב (hash).
מבנה קפדני של CSP
מדיניות Content Security בסיסית וקפדנית משתמשת באחד מהכותרות הבאות של תגובת HTTP:
CSP מחמיר שמבוסס על קוד חד-פעמי (nonce)
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
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 מחמיר, צריך:
- מחליטים אם להגדיר באפליקציה CSP שמבוסס על גיבוב (hash) או על צפן חד-פעמי.
- מעתיקים את ה-CSP מהקטע Strict CSP structure ומגדירים אותו ככותרת תגובה באפליקציה.
- מבצעים ריפרקטור של תבניות HTML וקוד בצד הלקוח כדי להסיר דפוסים שלא תואמים ל-CSP.
- פורסים את ה-CSP.
במהלך התהליך, תוכלו להשתמש בבדיקת שיטות מומלצות של Lighthouse (גרסה 7.3.0 ואילך עם הדגל --preset=experimental
) כדי לבדוק אם יש באתר שלכם CSP, ואם הוא מספיק מחמיר כדי למנוע XSS.
שלב 1: מחליטים אם צריך CSP שמבוסס על גיבוב או על צפן חד-פעמי
כך פועלים שני הסוגים של CSP מחמיר:
CSP מבוסס-nonce
ב-CSP שמבוסס על nonce, יוצרים מספר אקראי בזמן הריצה, כוללים אותו ב-CSP ומשייכים אותו לכל תג סקריפט בדף. תוקף לא יכול לכלול סקריפט זדוני בדף או להריץ אותו, כי הוא יצטרך לנחש את המספר האקראי הנכון לסקריפט הזה. האפשרות הזו פועלת רק אם המספר לא ניתן לניחוי, והוא נוצר מחדש בכל תגובה בזמן הריצה.
שימוש ב-CSP שמבוסס על nonce לדפי HTML שנעשה להם רינדור בשרת. בדפים האלה, אפשר ליצור מספר אקראי חדש לכל תגובה.
CSP מבוסס-גיבוב
ב-CSP שמבוסס על גיבוב, הגיבוב של כל תג סקריפט מוטמע מתווסף ל-CSP. לכל סקריפט יש גיבוב שונה. תוקף לא יכול לכלול סקריפט זדוני בדף או להריץ אותו, כי כדי להריץ אותו, ה-hash של הסקריפט צריך להיות ב-CSP.
משתמשים ב-CSP מבוסס-גיבוב (hash) לדפי HTML שמוצגים באופן סטטי, או לדפים שצריך לשמור במטמון. לדוגמה, אפשר להשתמש ב-CSP מבוסס-גיבוב לאפליקציות אינטרנט של דף יחיד שנוצרו באמצעות מסגרות כמו 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';
יצירת קוד חד-פעמי ל-CSP
מספר חד-פעמי הוא מספר אקראי שמשמש רק פעם אחת בכל טעינה של דף. אפשר להשתמש ב-CSP שמבוסס על קוד חד-פעמי כדי לצמצם את הסיכון ל-XSS רק אם תוקפים לא יכולים לנחש את ערך ה-nonce. מחרוזת חד-פעמית (nonce) של CSP חייבת להיות:
- ערך אקראי חזק מבחינה קריפטוגרפית (רצוי באורך של 128 ביט ומעלה)
- נוצר מחדש לכל תשובה
- בקידוד Base64
ריכזנו כאן כמה דוגמאות לאופן שבו אפשר להוסיף nonce של CSP במסגרות בצד השרת:
- Django (python)
- Express (JavaScript):
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
אפשר לטעון סקריפטים של צד שלישי באופן דינמי באמצעות סקריפט מוטמע.
<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>
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
שיקולים לגבי טעינת סקריפטים
בדוגמה של הסקריפט שמוצג בקוד, הוסיפו את 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) אוסר על שימוש בסימון מהסוג הזה.
אם האתר שלכם משתמש באחד מהדפוסים האלה, תצטרכו לבצע להם רפאקציה (refactor) ולהפוך אותם לחלופות בטוחות יותר.
אם הפעלתם את CSP בשלב הקודם, תוכלו לראות במסוף הפרות של CSP בכל פעם ש-CSP חוסם דפוס לא תואם.
ברוב המקרים, התיקון פשוט:
שינוי מבנה של גורמים שמטפלים באירועים בקוד
<span id="things">A thing.</span> <script nonce="${nonce}"> document.getElementById('things').addEventListener('click', doThings); </script>
<span onclick="doThings();">A thing.</span>
שינוי מבנה של מזהי URI מסוג javascript:
<a id="foo">foo</a> <script nonce="${nonce}"> document.getElementById('foo').addEventListener('click', linkClicked); </script>
<a href="javascript:linkClicked()">foo</a>
הסרת eval()
מקודק ה-JavaScript
אם באפליקציה שלכם נעשה שימוש ב-eval()
כדי להמיר סריאליזציות של מחרוזות JSON לאובייקטים של JS, עליכם לבצע רפאקציה של המופעים האלה ל-JSON.parse()
, שגם מהיר יותר.
אם אתם לא יכולים להסיר את כל השימושים ב-eval()
, עדיין תוכלו להגדיר מדיניות CSP מחמירה שמבוססת על קוד חד-פעמי, אבל תצטרכו להשתמש במילות המפתח של מדיניות CSP 'unsafe-eval'
, וכך רמת האבטחה של המדיניות תהיה נמוכה יותר.
דוגמאות נוספות לשיפורים כאלה מופיעות ב-Codelab הזה בנושא CSP קפדני:
שלב 4 (אופציונלי): מוסיפים חלופות לתמיכה בגרסאות ישנות של דפדפנים
אם אתם צריכים לתמוך בגרסאות ישנות יותר של הדפדפן:
- כדי להשתמש ב-
strict-dynamic
, צריך להוסיף אתhttps:
כחלופה לגרסאות קודמות של Safari. כשעושים את זה:- כל הדפדפנים שתומכים ב-
strict-dynamic
מתעלמים מהחלופהhttps:
, כך שהדבר לא יפחית את חוזק המדיניות. - בדפדפנים ישנים, סקריפטים שמקורם חיצוני יכולים להיטען רק אם הם מגיעים ממקור HTTPS. האפשרות הזו פחות מאובטחת ממדיניות CSP מחמירה, אבל היא עדיין מונעת חלק מהגורמים הנפוצים למתקפות XSS, כמו הזרקות של מזהי URI מסוג
javascript:
.
- כל הדפדפנים שתומכים ב-
- כדי להבטיח תאימות לגרסאות דפדפן ישנות מאוד (שנמצאות בשימוש כבר יותר מ-4 שנים), אפשר להוסיף את
unsafe-inline
כחלופה. כל הדפדפנים העדכניים מתעלמים מ-unsafe-inline
אם יש nonce או גיבוב של CSP.
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
שלב 5: פריסת ה-CSP
אחרי שתאשרו שה-CSP לא חוסם סקריפטים חוקיים בסביבת הפיתוח המקומית, תוכלו לפרוס את ה-CSP בסביבת ייצור, ואז בסביבת הייצור:
- (אופציונלי) פורסים את ה-CSP במצב דיווח בלבד באמצעות הכותרת
Content-Security-Policy-Report-Only
. מצב דיווח בלבד שימושי לבדיקה של שינוי שעלול לגרום לשיבושים, כמו הוספת ספק ניהול תוכן חדש בסביבת הייצור, לפני שמתחילים לאכוף את ההגבלות של ספק ניהול התוכן. במצב דיווח בלבד, ה-CSP לא משפיע על התנהגות האפליקציה, אבל הדפדפן עדיין יוצר שגיאות במסוף ודוחות על הפרות כשהוא נתקל בדפוסים שלא תואמים ל-CSP, כדי שתוכלו לראות מה היה נשבר אצל משתמשי הקצה. למידע נוסף, ראו Reporting API. - אחרי שתהיה לכם ודאות שה-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.
קריאה נוספת
- CSP Is Dead, Long Live CSP! על חוסר האבטחה של רשימות ההיתרים ועל העתיד של Content Security Policy
- כלי להערכת ספקי מודעות (CSP)
- LocoMoco Conference: Content Security Policy - A successful mess between hardening and mitigation
- שיחה ב-Google I/O: אבטחת אפליקציות אינטרנט באמצעות תכונות פלטפורמה מודרניות