מניעת נקודות חולשה של סקריפטים חוצי-אתרים המבוססים על 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 היא אחת מנקודות החולשה הנפוצות ביותר באבטחת האינטרנט, וצוותי הפיתוח נוטים להכניס אותה בטעות לאפליקציות שלהם. סוגים מהימנים נותנים לכם כלים לכתיבה, לבדיקת אבטחה ולשמירה על אפליקציות בלי נקודות חולשה של DOM XSS, על ידי אבטחת ברירת מחדל של פונקציות מסוכנות Web API. הסוגים המהימנים זמינים כ-polyfill לדפדפנים שעדיין לא תומכים בהם.

רקע

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

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

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

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

מבוא ל-API

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

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

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

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

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

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

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

קריאה נוספת