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

Krzysztof Kotowicz
Krzysztof Kotowicz

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: behind a flag.
  • 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 על ידי שרשור מחרוזות. במקום זאת, כדאי להשתמש בספריות תבניות עם בריחה אוטומטית בטוחה בהקשר, יחד עם מדיניות אבטחת תוכן מבוססת-ערך חד-פעמי לשיפור ההגנה מפני באגים.

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

מבוא ל-API

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

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

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

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

מה מומלץ לעשות
anElement.innerHTML = aTrustedHTML;
  
כשהתכונה Trusted Types מופעלת, הדפדפן מקבל אובייקט TrustedHTML למקורות שמצפים לקטעי 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, כדי שהדפדפן לא ייצור הפרה.

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

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

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

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

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

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

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

קריאה נוספת