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 קפדני מבוסס-גיבוב
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 מבוסס על צופן חד-פעמי (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 במסגרות בצד השרת:
- 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 שמבוסס על גיבוב או על מספר חד-פעמי אוסר על שימוש בסימון מהסוג הזה.
אם האתר שלכם משתמש באחד מהדפוסים האלה, תצטרכו לבצע להם רפאקציה (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'
CSP, ולכן המדיניות תהיה פחות מאובטחת.
תוכלו למצוא את הדוגמאות האלה ועוד דוגמאות ארגון מחדש (Refactoring) מהסוג הזה ב-Codelab המחמיר של CSP:
שלב 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 בסביבת ייצור, ואז בסביבת הייצור:
- (אופציונלי) פורסים את ה-CSP במצב דוח בלבד באמצעות הכותרת
Content-Security-Policy-Report-Only
. מצב דיווח בלבד שימושי לבדיקה של שינוי שעלול לגרום לשיבושים, כמו CSP חדש בסביבת הייצור, לפני שמתחילים לאכוף את ההגבלות של ה-CSP. במצב דיווח בלבד, ה-CSP לא משפיע על התנהגות האפליקציה, אבל הדפדפן עדיין יוצר שגיאות במסוף ודוחות על הפרות כשהוא נתקל בדפוסים שלא תואמים ל-CSP, כדי שתוכלו לראות מה היה נשבר אצל משתמשי הקצה. למידע נוסף, ראו Reporting API. - אחרי שתהיה לכם ודאות שה-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 שמשמשים לעתים רחוקות.
מפתחים ומהנדסי אבטחה צריכים לשים לב במיוחד לתבניות כאלה במהלך בדיקות הקוד ובדיקות האבטחה. פרטים נוספים על המקרים האלה זמינים במאמר מדיניות אבטחת תוכן: איך משלבים בין הקשחת אבטחה לבין הפחתת סיכונים.
קריאה נוספת
- תוכנית CSP Dead, Long Live CSP! על חוסר האבטחה של רשימות ההיתרים ועל העתיד של Content Security Policy
- כלי להערכת פלטפורמות צד שלישי (CSP)
- LocoMoco Conference: Content Security Policy - A successful mess between hardening and mitigation
- שיחה ב-Google I/O: אבטחת אפליקציות אינטרנט באמצעות תכונות פלטפורמה מודרניות