Browser Support
פרצת אבטחה XSS (cross-site scripting), שמאפשרת להחדיר סקריפטים זדוניים לאפליקציית אינטרנט, היא אחת מנקודות החולשה הגדולות ביותר באבטחת אינטרנט כבר יותר מעשור.
Content Security Policy (CSP) היא שכבת אבטחה נוספת שעוזרת לצמצם את הסיכון לפרצות אבטחה מסוג XSS. כדי להגדיר CSP, מוסיפים את כותרת ה-HTTP Content-Security-Policy לדף אינטרנט ומגדירים ערכים שקובעים אילו משאבים סוכן המשתמש יכול לטעון עבור הדף הזה.
בדף הזה מוסבר איך להשתמש ב-CSP שמבוסס על גיבובים או על צפנים חד-פעמיים כדי לצמצם את הסיכון ל-XSS, במקום ב-CSP שמבוסס על רשימת היתרים של מארחים, שמשמש בדרך כלל אבל לרוב משאיר את הדף חשוף ל-XSS כי אפשר לעקוף אותו ברוב ההגדרות.
מונח חשוב: צופן חד-פעמי (nonce) הוא מספר אקראי שמשמש רק פעם אחת, ואפשר להשתמש בו כדי לסמן תג <script> כמהימן.
מונח מרכזי: פונקציית גיבוב היא פונקציה מתמטית שממירה ערך קלט לערך מספרי דחוס שנקרא גיבוב. אפשר להשתמש בגיבוב (hash) (לדוגמה, SHA-256) כדי לסמן תג <script> מוטבע כ'מהימן'.
מדיניות Content Security Policy שמבוססת על ערכי nonce או על גיבוב נקראת לעיתים קרובות מדיניות CSP מחמירה. כשמשתמשים ב-CSP מחמיר, תוקפים שמאתרים פגמים בהחדרת HTML בדרך כלל לא יכולים להשתמש בהם כדי לחייב את הדפדפן להריץ סקריפטים זדוניים במסמך פגיע. הסיבה לכך היא שמדיניות CSP מחמירה מאפשרת רק סקריפטים עם גיבוב או סקריפטים עם ערך nonce נכון שנוצר בשרת, כך שתוקפים לא יכולים להריץ את הסקריפט בלי לדעת את ערך ה-nonce הנכון לתגובה מסוימת.
למה כדאי להשתמש במדיניות CSP מחמירה?
אם באתר שלכם כבר יש CSP שנראה כמו script-src www.googleapis.com,
סביר להניח שהוא לא יעיל נגד תקיפות חוצות אתרים. סוג כזה של CSP נקרא CSP של רשימת היתרים. הן דורשות התאמה אישית רבה, ותוקפים יכולים לעקוף אותן.
מדיניות CSP מחמירה שמבוססת על ערכי nonce או על גיבוב קריפטוגרפיים מאפשרת להימנע מהבעיות האלה.
מבנה CSP קפדני
מדיניות בסיסית מחמירה של אבטחת תוכן משתמשת באחת מכותרות התגובה הבאות של HTTP:
Nonce-based strict CSP
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none39;;
base-uri 'none';
CSP מחמיר שמבוסס על גיבוב (hashing)
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none39;;
base-uri 'none';
המאפיינים הבאים הופכים את מדיניות ה-CSP הזו ל'מחמירה' ולכן למאובטחת:
- היא משתמשת בערכי nonce
'nonce-{RANDOM}'או בערכי hash'sha256-{HASHED_INLINE_SCRIPT}'כדי לציין אילו תגי<script>מפתחי האתר סומכים עליהם שיפעלו בדפדפן של המשתמש. - היא מגדירה את
'strict-dynamic'כדי לצמצם את המאמץ שנדרש לפריסת CSP שמבוסס על גיבוב או על צופן חד-פעמי, על ידי מתן הרשאה אוטומטית להפעלת סקריפטים שנוצרו על ידי סקריפט מהימן. הפעולה הזו גם מבטלת את החסימה של רוב ספריות ה-JavaScript והווידג'טים של צד שלישי. - היא לא מבוססת על רשימות של כתובות URL שאפשר להשתמש בהן, ולכן היא לא סובלת מעקיפות נפוצות של CSP.
- היא חוסמת סקריפטים מוטבעים לא מהימנים, כמו גורמים מטפלים באירועים מוטבעים או מזהי URI של
javascript:. - היא מגבילה את
object-srcכדי להשבית תוספים מסוכנים כמו Flash. - היא מגבילה את
base-uriכדי לחסום את ההחדרה של תגי<base>. כך נמנע ממקורות תוקפים לשנות את המיקומים של סקריפטים שנטענים מכתובות URL יחסיות.
אימוץ CSP מחמיר
כדי להטמיע CSP מחמיר, צריך:
- מחליטים אם האפליקציה צריכה להגדיר CSP מבוסס-גיבוב (hash) או מבוסס-צופן חד-פעמי (nonce).
- מעתינים את ה-CSP מהקטע מבנה CSP מחמיר ומגדירים אותו ככותרת תגובה באפליקציה.
- מבצעים רפקטורינג בתבניות HTML ובקוד בצד הלקוח כדי להסיר דפוסים שלא תואמים ל-CSP.
- פורסים את ה-CSP.
אתם יכולים להשתמש בביקורת Best Practices של Lighthouse (גרסה 7.3.0 ואילך עם הדגל --preset=experimental) במהלך התהליך הזה כדי לבדוק אם באתר שלכם יש CSP, ואם הוא מספיק מחמיר כדי להיות יעיל נגד XSS.
שלב 1: מחליטים אם צריך CSP מבוסס-גיבוב (hash) או מבוסס-צופן חד-פעמי (nonce)
כך פועלים שני הסוגים של CSP מחמיר:
CSP מבוסס-Nonce
ב-CSP מבוסס-nonce, יוצרים מספר אקראי בזמן ריצה, כוללים אותו ב-CSP ומקשרים אותו לכל תג סקריפט בדף. תוקף לא יכול לכלול או להריץ סקריפט זדוני בדף שלכם, כי הוא יצטרך לנחש את המספר האקראי הנכון של הסקריפט הזה. השיטה הזו פועלת רק אם אי אפשר לנחש את המספר, והוא נוצר מחדש בזמן הריצה עבור כל תגובה.
שימוש ב-CSP מבוסס-nonce לדפי HTML שעברו עיבוד בשרת. בדפים האלה, אפשר ליצור מספר אקראי חדש לכל תגובה.
CSP מבוסס-גיבוב
ב-CSP מבוסס-גיבוב, הגיבוב של כל תג סקריפט מוטבע מתווסף ל-CSP. לכל סקריפט יש גיבוב שונה. תוקף לא יכול לכלול או להריץ סקריפט זדוני בדף שלכם, כי הגיבוב של הסקריפט הזה צריך להיות ב-CSP כדי שהוא יפעל.
משתמשים ב-CSP מבוסס-גיבוב (hash) לדפי HTML שמוצגים באופן סטטי, או לדפים שצריך לשמור במטמון. לדוגמה, אפשר להשתמש ב-CSP מבוסס-גיבוב לאפליקציות אינטרנט של דף יחיד שנבנו באמצעות מסגרות כמו Angular, React או אחרות, שמוצגות באופן סטטי ללא עיבוד בצד השרת.
שלב 2: הגדרת CSP מחמיר והכנת הסקריפטים
כשמגדירים CSP, יש כמה אפשרויות:
- מצב דוח בלבד (
Content-Security-Policy-Report-Only) או מצב אכיפה (Content-Security-Policy). במצב דוח בלבד, ה-CSP עדיין לא יחסום משאבים, כך ששום דבר באתר לא ייפגע, אבל תוכלו לראות שגיאות ולקבל דוחות על כל מה שהיה אמור להיחסם. באופן מקומי, כשמגדירים את ה-CSP, זה לא משנה כי שני המצבים מציגים את השגיאות במסוף הדפדפן. אם יש משאבים שה-CSP בטיוטה חוסם, מצב האכיפה יכול לעזור לכם למצוא אותם, כי חסימה של משאב עלולה לגרום לדף להיראות פגום. מצב דיווח בלבד יהיה שימושי יותר בהמשך התהליך (ראו שלב 5). - תג header או תג HTML
<meta>. בפיתוח מקומי, תג<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
ערך nonce הוא מספר אקראי שמשמש רק פעם אחת לכל טעינת דף. CSP מבוסס-nonce יכול לצמצם את הסיכון ל-XSS רק אם התוקפים לא יכולים לנחש את ערך ה-nonce. A ערך nonce של CSP חייב להיות:
- ערך אקראי חזק מבחינה קריפטוגרפית (באורך של 128 ביט ומעלה, באופן אידיאלי)
- נוצר מחדש בכל תשובה
- בקידוד Base64
הנה כמה דוגמאות להוספת nonce של CSP ב-frameworks בצד השרת:
- 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(&>quot;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. לכל הסקריפטים יכול להיות אותו ערך nonce. השלב הראשון הוא להוסיף את המאפיינים האלה לכל הסקריפטים כדי שמדיניות ה-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}'.
טעינה דינמית של סקריפטים ממקורות
אפשר לטעון סקריפטים של צד שלישי באופן דינמי באמצעות סקריפט מוטבע.
<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.hea<d.appen>dChild(s);
});
/script{HASHED_INLINE_SCRIPT}. כדי לצמצם את מספר הגיבובים, אפשר למזג את כל הסקריפטים המוטבעים לסקריפט אחד. כדי לראות את זה בפעולה, אפשר לעיין בדוגמה הזו ובקוד שלה.
<script src="https://example.org/fo><o.js&qu>o<t;/script script src="https://exam><ple.org>/bar.js"/script
integrity שתואם למקור מותר.
שיקולים לגבי טעינת סקריפטים
בדוגמה של סקריפט מוטבע, התג s.async = false נוסף כדי לוודא ש-foo יופעל לפני bar, גם אם bar נטען קודם. בקטע הקוד הזה, s.async = false
הניתוח לא נחסם בזמן שהסקריפטים נטענים, כי הסקריפטים
נוספים באופן דינמי. המנתח מפסיק רק בזמן שהסקריפטים פועלים, כמו במקרה של סקריפטים מסוג async. עם זאת, חשוב לזכור לגבי הקטע הזה:
-
יכול להיות שאחד מהסקריפטים או שניהם יופעלו לפני שההורדה של המסמך תסתיים. אם רוצים שהמסמך יהיה מוכן בזמן שהסקריפטים יופעלו, צריך להמתין לאירוע
DOMContentLoadedלפני שמצרפים את הסקריפטים. אם זה גורם לבעיה בביצועים כי הסקריפטים לא מתחילים להוריד מוקדם מספיק, כדאי להשתמש בתגי preload מוקדם יותר בדף. -
defer = trueלא עושה כלום. אם אתם צריכים את ההתנהגות הזו, אתם יכולים להריץ את הסקריפט באופן ידני כשצריך.
שלב 3: שינוי מבנה של תבניות HTML וקוד בצד הלקוח
אפשר להשתמש ב-handlers של אירועים מוטבעים (כמו onclick="…", onerror="…") וב-URI של JavaScript (<a href="javascript:…">) כדי להריץ סקריפטים. המשמעות היא שתוקף שמאתר באג XSS יכול להחדיר HTML מסוג כזה ולהריץ JavaScript זדוני. CSP שמבוסס על גיבוב או על צופן חד-פעמי אוסר על שימוש בתגי עיצוב מהסוג הזה.
אם באתר שלכם נעשה שימוש באחת מהתבניות האלה, תצטרכו לשנות את הקוד כך שישתמש בחלופות בטוחות יותר.
אם הפעלתם CSP בשלב הקודם, תוכלו לראות הפרות של CSP במסוף בכל פעם ש-CSP חוסם תבנית לא תואמת.
ברוב המקרים, הפתרון פשוט:
שינוי מבנה של גורמים מטפלים באירועים מוטבעים
<span id="th>ings&quo<t;A t>h<ing./span
script nonce=>"${nonce}"
document.getElementById('things').addEventL<istener>('click', doThings);
/script<span onclick="doThing>s();&quo<t;A t>hing./span
שינוי מבנה של מזהי URI javascript:
<a id=">;fo<o&>q<uot;foo/a
script nonce=>"${nonce}"
document.getElementById('foo').addEventList<ener(>39;click', linkClicked);
/script<a href="javascript:linkClick>ed(<)&>quot;foo/a
הסרה של eval() מ-JavaScript
אם האפליקציה שלכם משתמשת ב-eval() כדי להמיר סריאליזציות של מחרוזות JSON לאובייקטים של JS, כדאי לבצע רפקטורינג של מקרים כאלה ל-JSON.parse(), שהוא גם מהיר יותר.
אם אתם לא יכולים להסיר את כל השימושים ב-eval(), עדיין תוכלו להגדיר CSP מחמיר שמבוסס על nonce, אבל תצטרכו להשתמש במילת המפתח '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 9;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 לא מגן על האפליקציה בצורה טובה:
- אם אתם משתמשים ב-nonce לסקריפט, אבל יש הזרקה ישירות לגוף או לפרמטר
srcשל רכיב<script>. - אם יש הזרקות למיקומים של סקריפטים שנוצרו באופן דינמי (
document.createElement('script')), כולל לכל פונקציות הספריות (script) שיוצרות צמתי DOM על סמך ערכי הארגומנטים שלהן. זה כולל כמה ממשקי 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.