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

Krzysztof Kotowicz
Krzysztof Kotowicz

תמיכה בדפדפן

  • 83
  • 83
  • x
  • x

מקור

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

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

רקע

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

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

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

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

מבוא ל-API

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

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

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

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

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

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

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

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

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

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

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

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

מעכשיו, בכל פעם שסוגים מהימנים מזהים הפרה, הדפדפן שולח דוח ל-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, ולהסיר מטענים ייעודיים (payloads) של 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() שלה. הכללים המוגדרים כ-HTML-escape תווי < כדי למנוע יצירה של רכיבי 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 הוא הקוד באחד מכללי המדיניות שלכם, ואתם יכולים לנעול אותו עוד יותר על ידי הגבלת יצירת המדיניות.

קריאה נוספת