צמצום של סקריפטים חוצי-אתרים (XSS) באמצעות מדיניות Content Security Policy מחמירה (CSP)

תמיכה בדפדפן

  • Chrome: 52.
  • קצה: 79.
  • Firefox: 52.
  • Safari: 15.4.

מקור

סקריפטים חוצי-אתרים (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 מחמירה ולא מבוססת על צופן חד-פעמי

מדיניות 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 מחמירה, צריך:

  1. מחליטים אם האפליקציה צריכה להגדיר CSP על בסיס חד-פעמי או גיבוב (hash).
  2. מעתיקים את ה-CSP מהקטע Strict CSP build ומגדירים אותו. ככותרת תגובה בכל האפליקציה.
  3. ארגון מחדש של תבניות HTML וקוד בצד הלקוח כדי להסיר דפוסים לא תואמת ל-CSP.
  4. פורסים את ה-CSP.

ניתן להשתמש ב-Lighthouse (גרסה 7.3.0 ואילך עם הסימון --preset=experimental) ביקורת שיטות מומלצות לאורך התהליך הזה כדי לבדוק אם לאתר שלכם יש מדיניות CSP ואם הוא מחמירים מספיק כדי להיות יעילים נגד XSS.

מגדלור
  שהאזהרה בדוח היא שלא נמצאה מדיניות CSP במצב אכיפה.
אם לאתר שלכם אין מדיניות CSP, האזהרה הזו תוצג ב-Lighthouse.

שלב 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 לא תומכים במצב דוח בלבד.

אפשרות א': מדיניות 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 בצד השרת:

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 מאפשרת זאת.

אפשרות ב': כותרת תגובה של 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) לסקריפטים שמתקבלים לא נתמכים בצורה תקינה בכל הדפדפנים.

דוגמה להטמעת סקריפטים.
מותרת על ידי 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>
כדי להפעיל את הסקריפט הזה, צריך לחשב את הגיבוב (hash) של הסקריפט המוטבע ומוסיפים אותה לכותרת התגובה של CSP, מחליפים את {HASHED_INLINE_SCRIPT} placeholder. כדי להפחית את כמות הגיבובים, אפשר למזג את כל הפריטים בשורה את הסקריפטים לסקריפט אחד. כדי לראות את זה בפועל, דוגמה והקוד שלו.
נחסם על ידי CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
מדיניות CSP חוסמת את הסקריפטים האלה כי אפשר לבצע גיבוב רק של סקריפטים מוטבעים.

שיקולים לטעינת סקריפטים

הדוגמה של הסקריפט המוטבע מוסיפה 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 חוסם דפוס לא תואם.

דוחות על הפרות של CSP במסוף המפתחים של Chrome.
שגיאות במסוף לגבי קוד חסום.

ברוב המקרים, התיקון פשוט:

ארגון מחדש של הגורמים המטפלים באירועים מוטבעים

מותרת על ידי CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
מדיניות CSP מאפשרת גורמים מטפלים באירועים שרשומים באמצעות JavaScript.
נחסם על ידי CSP
<span onclick="doThings();">A thing.</span>
מדיניות CSP חוסמת את הגורמים המטפלים באירועים מוטבעים.

ארגון מחדש של javascript: מזהי URI

מותרת על ידי CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
מדיניות CSP מאפשרת גורמים מטפלים באירועים שרשומים באמצעות JavaScript.
נחסם על ידי CSP
<a href="javascript:linkClicked()">foo</a>
מדיניות CSP חוסמת JavaScript: URIs.

הסרת eval() מ-JavaScript

אם האפליקציה שלך משתמשת ב-eval() כדי להמיר סדרות JSON של מחרוזות JSON ל-JS של המכונות, צריך לארגן מחדש מכונות כאלה ל-JSON.parse(), מהר יותר.

אם לא ניתן להסיר את כל השימושים ב-eval(), עדיין אפשר להגדיר המרה מחמירה שלא מבוססת על CSP, אבל צריך להשתמש במילת המפתח 'unsafe-eval' CSP, המדיניות קצת פחות מאובטחת.

אפשר למצוא את הדוגמאות האלה ועוד דוגמאות ארגון מחדש (Refactoring) מהסוג הזה במדיניות ה-CSP המחמירה codelab:

שלב 4 (אופציונלי): הוספת חלופות לתמיכה בגרסאות דפדפן ישנות

תמיכה בדפדפן

  • Chrome: 52.
  • קצה: 79.
  • Firefox: 52.
  • Safari: 15.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, ולאחר מכן סביבת ייצור:

  1. (אופציונלי) פורסים את ה-CSP במצב דוח בלבד באמצעות הכותרת Content-Security-Policy-Report-Only. מצב 'דוח בלבד' זמין ל: לבדוק שינוי שעלול לגרום לכשל, כמו מדיניות CSP חדשה בסביבת הייצור, לפני להתחיל לאכוף הגבלות על מדיניות CSP. במצב דוח בלבד, ה-CSP לא משפיעה על התנהגות האפליקציה, אבל הדפדפן עדיין יוצר שגיאות במסוף ובדיווחים על הפרות כשהוא נתקל בדפוסים שלא תואמים ל-CSP, כך שתוכלו לראות מה היה עלול להיפגע מבחינת משתמשי הקצה. לקבלת מידע נוסף מידע נוסף זמין ב-Reporting API.
  2. כשאתם בטוחים שה-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: בלבול מוצלח בין הקשחה לבין הפחתת ההשפעה.

קריאה נוספת