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

Krzysztof Kotowicz
Krzysztof Kotowicz

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

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

רקע

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

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

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

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

מבוא ל-API

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

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

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

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

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

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

איך משתמשים ב-Trusted Types

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

אפשר לפרוס אוסף דוחות, כמו 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:

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 ולהסיר ממנו עומסי נתונים של XSS.

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

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

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

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

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

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

הקוד הזה יוצר מדיניות בשם myEscapePolicy שיכולה ליצור אובייקטים מסוג TrustedHTML באמצעות הפונקציה createHTML() שלה. הכללים שהוגדרו מבצעים בריחה מ-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 שמקבל רק את הסוג Trusted.

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

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

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

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

קריאה נוספת