Browser Support
פרצת אבטחה 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 קפדני שמבוסס על גיבוב (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 מחמיר, צריך:
- מחליטים אם האפליקציה צריכה להגדיר CSP שמבוסס על גיבוב (hash) או על צופן חד-פעמי.
- מעתיקים את ה-CSP מהקטע מבנה CSP מחמיר ומגדירים אותו ככותרת תגובה באפליקציה.
- משנים את המבנה של תבניות HTML וקוד בצד הלקוח כדי להסיר דפוסים שלא תואמים ל-CSP.
- פורסים את ה-CSP.
אתם יכולים להשתמש בבדיקת השיטות המומלצות של Lighthouse (גרסה 7.3.0 ואילך עם הדגל --preset=experimental) לאורך התהליך הזה כדי לבדוק אם באתר שלכם יש CSP, ואם הוא מחמיר מספיק כדי להיות יעיל נגד XSS.
שלב 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 לא תומכים במצב דוחות בלבד.
מגדירים את כותרת התגובה הבאה של 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 במסגרות בצד השרת:
- 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 שמבוסס על צופן חד-פעמי, כל רכיב <script> חייב לכלול מאפיין nonce שתואם לערך הצופן החד-פעמי האקראי שצוין בכותרת ה-CSP. לכל הסקריפטים יכול להיות אותו
nonce. השלב הראשון הוא להוסיף את המאפיינים האלה לכל הסקריפטים כדי שמדיניות ה-CSP תאפשר אותם.
מגדירים את כותרת התגובה הבאה של 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}'.
טעינה דינמית של סקריפטים ממקורות שונים
אתם יכולים לטעון סקריפטים של צד שלישי באופן דינמי באמצעות סקריפט מוטבע.
<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>{HASHED_INLINE_SCRIPT}. כדי לצמצם את כמות הגיבובים, אפשר למזג את כל הסקריפטים שמוטמעים בשורה לסקריפט אחד. כדי לראות איך זה עובד, אפשר לעיין בדוגמה הזו ובקוד שלה.
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
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 חוסם תבנית לא תואמת.
ברוב המקרים, התיקון פשוט:
שכתוב של גורמים מטפלים באירועים מוטבעים
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things').addEventListener('click', doThings);
</script><span onclick="doThings();">A thing.</span>
Refactor javascript: URIs
<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 מחמירה שמבוססת על מספר חד-פעמי, אבל אתם צריכים להשתמש במילת המפתח 'unsafe-eval' של CSP, מה שהופך את המדיניות שלכם למעט פחות מאובטחת.
דוגמאות נוספות לארגון הקוד מחדש כזה אפשר למצוא ב-Codelab הזה של CSP מחמיר:
שלב 4 (אופציונלי): הוספת חלופות לתמיכה בגרסאות דפדפן ישנות
Browser Support
אם אתם צריכים לתמוך בגרסאות ישנות יותר של דפדפנים:
- כדי להשתמש ב-
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 ואז בסביבת הייצור:
- (אופציונלי) פורסים את ה-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 לא מגן על האפליקציה שלכם כמו שצריך:
- אם אתם מוסיפים 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 מפורט מידע נוסף על המקרים האלה.