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