סקריפטים חוצי-אתרים (XSS), היכולת להחדיר סקריפטים זדוניים לאפליקציית אינטרנט הייתה אחת נקודות החולשה הגדולות ביותר באבטחת אינטרנט במשך יותר מעשור.
Content Security Policy (CSP)
היא שכבת אבטחה נוספת שעוזרת לצמצם את ה-XSS. כדי להגדיר CSP:
להוסיף את כותרת ה-HTTP Content-Security-Policy
לדף אינטרנט ולהגדיר ערכים
אפשר לקבוע אילו משאבים סוכן המשתמש יכול לטעון בדף הזה.
בדף הזה נסביר איך להשתמש ב-CSP על סמך נתונים חד-פעמיים (hash) או גיבובים (hash) כדי לצמצם את ה-XSS. במקום ה-CSPs הנפוצים שמבוססים על רשימת ההיתרים של המארחים שיוצאים מהדף לעיתים קרובות נחשפו ל-XSS כי ניתן לעקוף אותם ברוב התצורות.
מונח מפתח: צומת הוא מספר אקראי שמשמש רק פעם אחת ושניתן להשתמש בו כדי לסמן
תג <script>
כמהימן.
מונח מפתח: פונקציית גיבוב (hash) היא פונקציה מתמטית שממירה קלט
לערך מספרי דחוס שנקרא גיבוב. אפשר להשתמש בגיבוב (hash)
(לדוגמה, SHA-256) כדי לסמן בתוך שורה
תג <script>
כמהימן.
מדיניות אבטחת תוכן שמבוססת על צפנים חד-פעמיים (hash) או גיבובים (hash) נקראת בדרך כלל מדיניות CSP מחמירה. כשאפליקציה משתמשת ב-CSP מחמירה, תוקפים שמוצאים HTML בדרך כלל פגמים בהחדרה לא יכולים להשתמש בהם כדי לאלץ את הדפדפן לבצע סקריפטים זדוניים במסמך פגיע. הסיבה לכך היא שרק מדיניות CSP מחמירה מאפשר סקריפטים או סקריפטים מגובבים עם ערך צופן חד-פעמי שנוצר ולכן תוקפים לא יכולים להריץ את הסקריפט בלי לדעת מהו הצופן הנכון (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 כמו זו ל"קפדנית" ולכן הם מאובטחים:
- נעשה בו שימוש בגיבובים (hash) מסוג
'nonce-{RANDOM}'
או בגיבובים (hash) מסוג'sha256-{HASHED_INLINE_SCRIPT}'
כדי לציין באילו תגי<script>
שהמפתח של האתר סומכים עליהם להפעלה בדפדפן של המשתמש. - היא מגדירה את
'strict-dynamic'
כדי לצמצם את המאמץ של פריסה של CSP על בסיס צפנים חד-פעמיים (hash) או גיבוב (hash) באמצעות שמאפשרות הפעלה של סקריפטים שסקריפט מהימן יוצר. גם מבטל את חסימת השימוש ברוב ספריות וווידג'טים של JavaScript של צד שלישי. - היא לא מבוססת על רשימות היתרים של כתובות URL, לכן היא לא משפיעה מעקפים נפוצים של CSP.
- הוא חוסם סקריפטים מוטבעים לא מהימנים, כמו גורמים מטפלים באירועים מוטבעים או
javascript:
מזהי URI. - הוא מגביל את
object-src
להשבתת יישומי פלאגין מסוכנים כמו Flash. - הוא מגביל את
base-uri
כדי לחסום את החדרת תגי<base>
. דבר זה מונע תוקפים מפני שינוי המיקומים של סקריפטים שנטענים מכתובות URL יחסיות.
מאמצים מדיניות CSP מחמירה
כדי ליישם מדיניות CSP מחמירה, צריך:
- מחליטים אם האפליקציה צריכה להגדיר CSP על בסיס חד-פעמי או גיבוב (hash).
- מעתיקים את ה-CSP מהקטע Strict CSP build ומגדירים אותו. ככותרת תגובה בכל האפליקציה.
- ארגון מחדש של תבניות HTML וקוד בצד הלקוח כדי להסיר דפוסים לא תואמת ל-CSP.
- פורסים את ה-CSP.
ניתן להשתמש ב-Lighthouse
(גרסה 7.3.0 ואילך עם הסימון --preset=experimental
) ביקורת שיטות מומלצות
לאורך התהליך הזה כדי לבדוק אם לאתר שלכם יש מדיניות CSP ואם הוא
מחמירים מספיק כדי להיות יעילים נגד XSS.
שלב 1: מחליטים אם צריך CSP מבוסס על צופן חד-פעמי (hash) או גיבוב (hash)
כך פועלים שני הסוגים של מדיניות CSP מחמירה:
מדיניות CSP מבוססת-צופן
באמצעות CSP שמבוסס על צופן חד-פעמי, יוצרים מספר אקראי בזמן הריצה, ולשייך אותו לכל תג סקריפט בדף. תוקף לא יכולות לכלול או להריץ סקריפט זדוני בדף שלכם, מפני שהן יצטרכו לנחש את המספר האקראי הנכון של הסקריפט. זה פועל רק אם המספר לא ניתן לנחש, והוא נוצר בזמן הריצה של כל תשובה.
שימוש במדיניות CSP שמבוססת על צופן חד-פעמי (CSP) לדפי HTML שמעובדים בשרת. לגבי הדפים האלה, תוכלו ליצור מספר אקראי חדש לכל תשובה.
מדיניות CSP שמבוססת על גיבוב
אם מדובר במדיניות CSP שמבוססת על גיבוב, הגיבוב של כל תג סקריפט מוטבע מתווסף ל-CSP. לכל סקריפט יש גיבוב (hash) שונה. תוקף לא יכול לכלול או להפעיל תוכנה זדונית בדף, מכיוון שה-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>
. לפיתוח מקומי, תג<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 יכול לצמצם את ה-XSS רק אם התוקפים לא יכולים לנחש את הערך חד-פעמי. א' צופן חד-פעמי (nonce) של CSP חייב להיות:
- ערך אקראי חזק מבחינה קריפטוגרפית (רצוי באורך של יותר מ-128 ביט)
- נוצר חדש לכל תשובה
- קידוד Base64
לפניכם כמה דוגמאות לדרכים שבהן מוסיפים צופן חד-פעמי של CSP ב-frameworks בצד השרת:
- ג'נגו (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 מאפשרת זאת.
הגדרה של תגובת ה-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 נתמכים בדפדפנים שונים רק לסקריפטים מוטבעים, אם משתמשים בסקריפט מוטבע, צריך לטעון את כל הסקריפטים של צד שלישי באופן דינמי. גיבובים (hash) לסקריפטים שמתקבלים לא נתמכים בצורה תקינה בכל הדפדפנים.
<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 שמבוססת על צופן חד-פעמי (hash) או גיבוב (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>
ארגון מחדש של javascript:
מזהי URI
<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 של מחרוזות JSON ל-JS
של המכונות, צריך לארגן מחדש מכונות כאלה ל-JSON.parse()
,
מהר יותר.
אם לא ניתן להסיר את כל השימושים ב-eval()
, עדיין אפשר להגדיר המרה מחמירה שלא מבוססת על
CSP, אבל צריך להשתמש במילת המפתח 'unsafe-eval'
CSP,
המדיניות קצת פחות מאובטחת.
אפשר למצוא את הדוגמאות האלה ועוד דוגמאות ארגון מחדש (Refactoring) מהסוג הזה במדיניות ה-CSP המחמירה codelab:
שלב 4 (אופציונלי): הוספת חלופות לתמיכה בגרסאות דפדפן ישנות
אם אתם צריכים לתמוך בגרסאות דפדפן ישנות יותר:
- כדי להשתמש בפונקציה
strict-dynamic
צריך להוסיף אתhttps:
כחלופה גרסאות של Safari. כשעושים זאת:- בכל הדפדפנים שתומכים ב-
strict-dynamic
מתעלמים מהחלופהhttps:
, כך שהדבר לא יפחית את חוזק המדיניות. - בדפדפנים ישנים, סקריפטים שנובעים מגורמים חיצוניים יכולים להיטען רק אם הם מגיעים
מקור HTTPS. זו שיטה פחות מאובטחת ממדיניות CSP מחמירה, אבל היא עדיין
מונע כמה סיבות נפוצות ל-XSS כמו החדרות של
javascript:
URI.
- בכל הדפדפנים שתומכים ב-
- כדי להבטיח תאימות לגרסאות ישנות מאוד של דפדפן (מעל 4 שנים), אפשר להוסיף
unsafe-inline
כחלופה. כל הדפדפנים האחרונים מתעלמים מ-unsafe-inline
אם קיים גיבוב (hash) או צופן חד-פעמי (hash) של 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 שמשמש אותך (צפנים, גיבובים, עם או בלי 'strict-dynamic'
), יש
הם מקרים שבהם CSP לא מגן גם על האפליקציה שלכם:
- אם לא מקודדים סקריפט, אבל מקבלים הזרקה ישירות לגוף,
הפרמטר
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: בלבול מוצלח בין הקשחה לבין הפחתת ההשפעה.