מניעת נקודות חולשה של סקריפטים חוצי-אתרים המבוססים על DOM באמצעות סוגים מהימנים

Krzysztof Kotowicz
Krzysztof Kotowicz

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox Technology Preview: supported.
  • Safari: 26.

Source

סקריפטינג חוצה אתרים (XSS) מבוסס-DOM מתרחש כשנתונים ממקור בשליטת המשתמש (כמו שם משתמש או כתובת URL להפניה שנלקחת מקטע כתובת ה-URL) מגיעים ליעד, שהוא פונקציה כמו eval() או מאפיין setter כמו .innerHTML שיכולים להריץ קוד JavaScript שרירותי.

‫DOM XSS היא אחת מהפגיעויות הנפוצות ביותר באבטחת אתרים, ולעתים קרובות צוותי פיתוח מכניסים אותה בטעות לאפליקציות שלהם. ‫Trusted Types מספק לכם את הכלים לכתוב, לבדוק את האבטחה ולשמור על אפליקציות ללא פרצות אבטחה מסוג DOM XSS, על ידי אבטחה של פונקציות מסוכנות של Web API כברירת מחדל. סוגים מהימנים זמינים כפוליפיל לדפדפנים שעדיין לא תומכים בהם.

רקע

במשך שנים רבות, DOM XSS הייתה אחת מנקודות החולשה הנפוצות והמסוכנות ביותר באבטחת אתרים.

יש שני סוגים של פרצות אבטחה XSS‏ (cross-site scripting). חלק מנקודות החולשה של XSS נגרמות בגלל קוד בצד השרת שיוצר את קוד ה-HTML שמרכיב את האתר בצורה לא מאובטחת. אחרות נובעות מבעיה בלקוח, שבה קוד JavaScript קורא לפונקציות מסוכנות עם תוכן שהמשתמש שולט בו.

כדי למנוע XSS בצד השרת, אל תיצרו HTML על ידי שרשור מחרוזות. במקום זאת, כדאי להשתמש בספריות תבניות עם בריחה אוטומטית בטוחה בהקשר, יחד עם מדיניות אבטחת תוכן מבוססת-nonce כדי לצמצם עוד יותר את הסיכון לבאגים.

עכשיו דפדפנים יכולים גם לעזור למנוע פרצות אבטחה מסוג XSS מבוסס-DOM בצד הלקוח באמצעות Trusted Types.

מבוא ל-API

התכונה 'סוגים מהימנים' פועלת על ידי נעילה של פונקציות מסוכנות של יעד. יכול להיות שחלק מהם כבר מוכרים לכם, כי ספקי דפדפנים ומסגרות אינטרנט כבר מונעים מכם להשתמש בתכונות האלה מסיבות אבטחה.

כדי להשתמש ב-Trusted Types, צריך לעבד את הנתונים לפני שמעבירים אותם לפונקציות האלה. השימוש רק במחרוזת נכשל, כי הדפדפן לא יודע אם הנתונים מהימנים:

מה אסור לעשות
anElement.innerHTML  = location.href;
אם Trusted Types מופעל, הדפדפן מחזיר TypeError ומונע שימוש ב-DOM XSS sink עם מחרוזת.

כדי לציין שהנתונים עברו עיבוד מאובטח, צריך ליצור אובייקט מיוחד – Trusted Type.

מה מומלץ לעשות
anElement.innerHTML = aTrustedHTML;
  
כשהתכונה Trusted Types מופעלת, הדפדפן מקבל אובייקט TrustedHTML עבור sinks שמצפים לקטעי HTML. יש גם אובייקטים של TrustedScript ו-TrustedScriptURL עבור יעדים רגישים אחרים.

השימוש ב-Trusted Types מפחית באופן משמעותי את שטח הפנים של המתקפה מסוג DOM XSS באפליקציה. היא מפשטת את בדיקות האבטחה ומאפשרת לאכוף את בדיקות האבטחה מבוססות-הסוג שמתבצעות בזמן קומפילציה, ניתוח קוד או איגוד קוד בזמן ריצה בדפדפן.

איך משתמשים בסוגים מהימנים

הכנה לדוחות על הפרות של מדיניות אבטחת תוכן

אפשר להטמיע כלי לאיסוף דוחות, כמו reporting-api-processor או go-csp-collector בקוד פתוח, או להשתמש באחד מהכלים המסחריים המקבילים. אפשר גם להוסיף רישום ביומן בהתאמה אישית ולנפות הפרות באגים בדפדפן באמצעות ReportingObserver:

const observer = new ReportingObserver((reports, observer) => {
    for (const report of reports) {
        if (report.type !== 'csp-violation' ||
            report.body.effectiveDirective !== 'require-trusted-types-for') {
            continue;
        }

        const violation = report.body;
        console.log('Trusted Types Violation:', violation);

        // ... (rest of your logging and reporting logic)
    }
}, { buffered: true });

observer.observe();

או על ידי הוספת פונקציית event listener:

document.addEventListener('securitypolicyviolation',
    console.error.bind(console));

הוספת כותרת CSP לדיווח בלבד

מוסיפים את כותרת התגובה הבאה של HTTP למסמכים שרוצים להעביר ל-Trusted Types:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

עכשיו כל ההפרות מדווחות ל-//my-csp-endpoint.example, אבל האתר ממשיך לפעול. בקטע הבא מוסבר איך פועל //my-csp-endpoint.example.

זיהוי הפרות של סוגים מהימנים

מעכשיו, בכל פעם ש-Trusted Types מזהה הפרה, הדפדפן שולח דוח ל-report-uri שהוגדר. לדוגמה, כשהאפליקציה שלכם מעבירה מחרוזת ל-innerHTML, הדפדפן שולח את הדוח הבא:

{
"csp-report": {
    "document-uri": "https://my.url.example",
    "violated-directive": "require-trusted-types-for",
    "disposition": "report",
    "blocked-uri": "trusted-types-sink",
    "line-number": 39,
    "column-number": 12,
    "source-file": "https://my.url.example/script.js",
    "status-code": 0,
    "script-sample": "Element innerHTML <img src=x"
}
}

השורה הזו מציינת שבקובץ https://my.url.example/script.js בשורה 39, הפונקציה innerHTML נקראה עם המחרוזת שמתחילה ב-<img src=x. המידע הזה אמור לעזור לכם לצמצם את האפשרויות ולזהות אילו חלקים בקוד עלולים לגרום ל-DOM XSS וצריך לשנות.

תיקון ההפרות

יש כמה אפשרויות לתיקון הפרה של סוג מהימן. אפשר להסיר את הקוד הבעייתי, להשתמש בספרייה, ליצור מדיניות של סוג מהימן או, כמוצא אחרון, ליצור מדיניות ברירת מחדל.

לשכתב את הקוד הבעייתי

יכול להיות שהקוד שלא עומד בדרישות כבר לא נחוץ, או שאפשר לכתוב אותו מחדש בלי הפונקציות שגורמות להפרות:

מה מומלץ לעשות
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
מה אסור לעשות
el.innerHTML = '<img src=xyz.jpg>';

שימוש בספרייה

חלק מהספריות כבר יוצרות סוגים מהימנים שאפשר להעביר לפונקציות של sink. לדוגמה, אפשר להשתמש ב-DOMPurify כדי לבצע סניטציה לקטע קוד HTML ולהסיר ממנו מטען ייעודי (payload) של XSS.

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});

‫DOMPurify תומך בסוגים מהימנים ומחזיר HTML שעבר סניטציה ועטוף באובייקט TrustedHTML, כדי שהדפדפן לא ייצור הפרה.

יצירת מדיניות של סוגים מהימנים

לפעמים אי אפשר להסיר את הקוד שגורם להפרה, ואין ספריה שאפשר להשתמש בה כדי לבצע סניטציה של הערך וליצור סוג מהימן בשבילכם. במקרים כאלה, אתם יכולים ליצור אובייקט מסוג מהימן בעצמכם.

קודם כל, יוצרים מדיניות. מדיניות היא מפעל לייצור סוגים מהימנים שמחיל כללי אבטחה מסוימים על הקלט שלהם:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: string => string.replace(/\</g, '&lt;')
  });
}

הקוד הזה יוצר מדיניות בשם myEscapePolicy שיכולה ליצור אובייקטים של TrustedHTML באמצעות הפונקציה createHTML() שלה. הכללים המוגדרים מבצעים escape לתווים < ב-HTML כדי למנוע יצירה של רכיבי HTML חדשים.

כך משתמשים במדיניות:

const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x onerror=alert(1)>'

שימוש במדיניות ברירת מחדל

לפעמים אי אפשר לשנות את הקוד הבעייתי, למשל אם טוענים ספרייה של צד שלישי מ-CDN. במקרה כזה, צריך להשתמש במדיניות ברירת מחדל:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  trustedTypes.createPolicy('default', {
    createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
  });
}

המדיניות שנקראת default משמשת בכל מקום שבו נעשה שימוש במחרוזת ב-sink שמקבל רק סוג מהימן.

מעבר לאכיפה של Content Security Policy

כשהאפליקציה שלכם לא מייצרת יותר הפרות, אתם יכולים להתחיל לאכוף Trusted Types:

Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

עכשיו, לא משנה כמה מורכבת אפליקציית האינטרנט שלכם, הדבר היחיד שיכול ליצור פגיעות מסוג DOM XSS הוא הקוד באחד מכללי המדיניות שלכם, ואתם יכולים להגביל את זה עוד יותר על ידי הגבלת יצירת כללי מדיניות.

קריאה נוספת