การแสดง HTML ที่ผู้ใช้สร้างขึ้นอย่างปลอดภัยเป็นปัญหาด้านความปลอดภัยที่พบได้ทั่วไป Sanitizer API แบบเนทีฟมีวิธีที่เพิ่มประสิทธิภาพเบราว์เซอร์ในตัวเพื่อตัดเนื้อหาที่เป็นอันตราย (เช่น แท็ก <script> หรือแอตทริบิวต์ on*) ก่อนที่จะเข้าถึง DOM
การทำความสะอาดฝั่งไคลเอ็นต์จะปกป้องหน้าเว็บที่แสดงผลเนื้อหา หากมีการแสดงผลเนื้อหาที่ผู้ใช้สร้างขึ้นเดียวกันนี้ในที่อื่นๆ ด้วย (ไคลเอ็นต์อื่นๆ, หน้าที่แสดงผลฝั่งเซิร์ฟเวอร์ หรืออีเมล) ให้ทำความสะอาดเนื้อหาในเซิร์ฟเวอร์ด้วยเพื่อเป็นการป้องกันเชิงลึก และตรวจสอบว่ากฎการทำความสะอาดของเซิร์ฟเวอร์และไคลเอ็นต์ไม่ขัดแย้งกัน
1. การแทรก HTML อย่างปลอดภัยด้วย setHTML()
วิธีหลักในการใช้ Sanitizer API คือผ่านเมธอด 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 ที่กำหนดเอง
คุณปรับแต่งกฎการล้างข้อมูลได้โดยสร้างอินสแตนซ์ 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 ซึ่งอาจทำให้เกิด Mutation 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() ไม่จำเป็นต้องมีนโยบายประเภทที่เชื่อถือได้ แต่ฟังก์ชันสำรองจำเป็นต้องมี โดยส่ง RETURN_TRUSTED_TYPE: true ไปยัง DOMPurify.sanitize() และอนุญาตชื่อนโยบาย dompurify ในคำสั่ง CSP trusted-types