הצגה בטוחה של HTML שנוצר על ידי משתמשים היא אתגר אבטחה נפוץ. Sanitizer API המקורי מספק דרך מובנית, שמותאמת לדפדפן, להסיר תוכן מסוכן (כמו תגי <script> או מאפייני on*) לפני שהוא מגיע ל-DOM.
הסרת תוכן זדוני או רגיש בצד הלקוח מגינה על הדף שמעבד את התוכן. אם אותו תוכן שנוצר על ידי משתמשים מוצג גם במקום אחר (לקוחות אחרים, דפים שמוצגים בשרת או אימיילים), צריך להסיר תוכן זדוני או רגיש בשרת וגם להשתמש בהגנה לעומק, ולוודא שכללי הסרת התוכן הזדוני או הרגיש בשרת ובלקוח לא מתנגשים.
1. הוספה בטוחה של HTML באמצעות setHTML()
הדרך העיקרית להשתמש ב-Sanitizer API היא באמצעות ה-method setHTML() בכל Element. השיטה הזו מטפלת בניתוח, בהסרת תוכן זדוני או רגיש ובהוספה בשלב אחד, וכך מבטיחה שהתוכן יעבור הסרת תוכן זדוני או רגיש באופן ספציפי להקשר שבו הוא יתווסף.
const untrustedHTML = `
<p>Hello!</p>
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<div onclick="doBadThing()">Click me</div>
`;
const container = document.getElementById('output');
// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);
2. שימוש בהגדרות מותאמות אישית של הסרת תוכן זדוני או רגיש
כדי להתאים אישית את כללי הסרת התוכן הזדוני או הרגיש, אתם יכולים ליצור מכונה של Sanitizer. כך אתם יכולים להגדיר בדיוק אילו רכיבים ומאפיינים מותרים לתרחיש הספציפי שלכם.
כשמשתמשים ב-Element.setHTML() וב-Document.parseHTML(), הגדרות ברירת המחדל של Sanitizer מסירות את כל הרכיבים והמאפיינים שלא בטוחים ל-XSS. אי אפשר ליצור Sanitizer מתירנית יותר שמאפשרת את הרכיבים האלה, אבל אפשר להגביל עוד יותר את מה שמותר.
// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
elements: ["p", "b", "i", "strong", "em"],
attributes: ["class"],
replaceWithChildrenElements: ['div']
};
const mySanitizer = new Sanitizer(config);
// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });
רכיבים שלא נמצאים ב-elements יוסרו יחד עם התוכן שלהם. מפרטים את רכיבי ה-wrapper שהטקסט שלהם צריך להישאר (כמו div למעלה) ב-replaceWithChildrenElements, שמסיר את הרכיב אבל משאיר את הצאצאים שלו.
אם אתם צריכים Sanitizer מתירני יותר שמאפשר רכיבים או מאפיינים מסוימים שלא בטוחים ל-XSS, אתם יכולים להשתמש ב-Sanitizer בהתאמה אישית עם Element.setHTMLUnsafe() ו-Document.parseHTMLUnsafe().
3. ניתוח קלט של משתמשים באמצעות Document.parseHTML()
אם אתם צריכים לנתח HTML לא מהימן בלי להוסיף אותו מיד ל-DOM הפעיל, השתמשו ב-Document.parseHTML(). השיטה הזו מחזירה Document שעבר הסרת תוכן זדוני או רגיש בהתאם לכללי ברירת המחדל או לכללים מותאמים אישית, כדי שתוכלו לבדוק או לשנות את הצמתים שעברו הסרת תוכן זדוני או רגיש (לדוגמה, כדי ליצור תצוגה מקדימה או לחלץ טקסט) לפני שאתם מוסיפים אותם.
מעבירים את הצמתים שנוצרו לדף במקום להפוך אותם למחרוזת ולנתח אותה מחדש באמצעות innerHTML, מה שעלול לגרום שוב ל-XSS מסוג מוטציה (mXSS).
const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';
const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);
חלופות ותמיכה בדפדפן ב-Sanitizer API
אם ה-Sanitizer API המקורי לא זמין בדפדפני היעד, צריך לטעון ספרייה כמו DOMPurify רק כשאין תמיכה מקורית, כדי להימנע משליחה שלה לדפדפנים שלא זקוקים לה. כדאי להשתמש בבדיקות in לזיהוי תכונות, כדי שהבדיקה עצמה תפעל בדפדפנים ישנים יותר.
let domPurifyPromise;
function loadDOMPurify() {
// Code-split with your bundler so DOMPurify is only downloaded when needed
domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
return domPurifyPromise;
}
export async function safeSetHTML(el, html) {
const elements = ['p', 'b', 'i', 'strong', 'em'];
const attributes = ['class'];
if ('setHTML' in Element.prototype) {
// setHTML() accepts a plain configuration object as the sanitizer
el.setHTML(html, { sanitizer: { elements, attributes } });
} else {
const DOMPurify = await loadDOMPurify();
el.innerHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [...elements, '#text'],
ALLOWED_ATTR: attributes,
// Match the native Sanitizer: strip aria-*/data-* and disallowed element content
ALLOW_ARIA_ATTR: false,
ALLOW_DATA_ATTR: false,
KEEP_CONTENT: false
});
}
}
כברירת מחדל, DOMPurify שומר על המאפיינים aria-* ו-data-* (גם כשמצוין ALLOWED_ATTR) וגם על תוכן הטקסט של הרכיבים שהוא מסיר, ואילו Sanitizer המקורי מסיר את המאפיינים aria-* ו-data-* ומסיר רכיבים אסורים יחד עם התוכן שלהם. הגדרת ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false ומתן הרשאה ל-#text גורמים ל-DOMPurify להתאים להתנהגות המקורית. ל-DOMPurify אין אפשרות מובנית שוות ערך ל-replaceWithChildrenElements, לכן צריך להימנע מהאפשרות הזו בהגדרות שדורשות חלופה תואמת.
אם הדף אוכף סוגים מהימנים (require-trusted-types-for 'script'), הקצאת מחרוזת ל-innerHTML גורמת לשגיאה. ל-setHTML() לא נדרשת מדיניות Trusted Types, אבל לברירת המחדל כן: צריך להעביר את RETURN_TRUSTED_TYPE: true אל DOMPurify.sanitize() ולאפשר את שם המדיניות dompurify שלו בהוראת ה-CSP של trusted-types.