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

Krzysztof Kotowicz
Krzysztof Kotowicz

תמיכה בדפדפנים

  • Chrome: 83.
  • Edge: ‏ 83.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

פריצה מסוג DOM-based cross-site scripting‏ (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 לקטגוריות אחרות של מידע רגיש.

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

איך משתמשים ב-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 הוא הקוד באחד מהכללי המדיניות שלכם, ואפשר לנעול אותו עוד יותר על ידי הגבלת יצירת המדיניות.

קריאה נוספת