Việc hiển thị HTML do người dùng tạo một cách an toàn là một thách thức bảo mật phổ biến. API Trình dọn dẹp gốc cung cấp một cách tích hợp, được tối ưu hoá cho trình duyệt để loại bỏ nội dung nguy hiểm (như thẻ <script> hoặc thuộc tính on*) trước khi nội dung đó đến DOM.
Tính năng dọn dẹp phía máy khách bảo vệ trang hiển thị nội dung. Nếu cùng một nội dung do người dùng tạo cũng được hiển thị ở nơi khác (ứng dụng khác, trang do máy chủ hiển thị hoặc email), hãy dọn dẹp nội dung đó trên máy chủ cũng như phòng thủ chuyên sâu, đồng thời đảm bảo các quy tắc dọn dẹp của máy chủ và ứng dụng không xung đột.
1. Chèn HTML một cách an toàn bằng setHTML()
Cách chính để sử dụng Sanitizer API là thông qua phương thức setHTML() trên bất kỳ Element nào. Phương thức này xử lý việc phân tích cú pháp, dọn dẹp và chèn trong một bước duy nhất, đảm bảo rằng nội dung được dọn dẹp cụ thể cho bối cảnh mà nội dung đó sẽ được chèn.
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. Sử dụng cấu hình trình dọn dẹp tuỳ chỉnh
Bạn có thể tuỳ chỉnh các quy tắc dọn dẹp bằng cách tạo một thực thể Sanitizer. Nhờ đó, bạn có thể xác định chính xác những phần tử và thuộc tính nào được phép cho trường hợp sử dụng cụ thể của mình.
Khi sử dụng Element.setHTML() và Document.parseHTML(), chế độ cài đặt Sanitizer mặc định sẽ xoá tất cả các phần tử và thuộc tính không an toàn cho XSS. Bạn không thể tạo một Sanitizer dễ dãi hơn để cho phép các phần tử đó, nhưng bạn có thể hạn chế thêm những gì được phép.
// 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 });
Các phần tử không có trong elements sẽ bị xoá cùng với nội dung của chúng. Liệt kê các phần tử bao bọc có văn bản mà bạn muốn giữ lại (chẳng hạn như div ở trên) trong replaceWithChildrenElements. Thao tác này sẽ xoá phần tử nhưng vẫn giữ lại các phần tử con của phần tử đó.
Nếu cần một Sanitizer dễ dãi hơn, cho phép một số phần tử hoặc thuộc tính không an toàn với XSS, bạn có thể sử dụng Trình dọn dẹp tuỳ chỉnh với Element.setHTMLUnsafe() và Document.parseHTMLUnsafe().
3. Phân tích cú pháp dữ liệu đầu vào của người dùng bằng Document.parseHTML()
Nếu bạn cần phân tích cú pháp HTML không đáng tin cậy mà không cần chèn ngay vào DOM đang hoạt động, hãy sử dụng Document.parseHTML(). Phương thức này trả về một Document đã được dọn dẹp theo các quy tắc mặc định hoặc tuỳ chỉnh, nhờ đó, bạn có thể kiểm tra hoặc chuyển đổi các nút đã dọn dẹp (ví dụ: để tạo bản xem trước hoặc trích xuất văn bản) trước khi chèn các nút đó.
Di chuyển các nút kết quả vào trang thay vì chuyển đổi tuần tự các nút đó thành một chuỗi và phân tích cú pháp lại bằng innerHTML. Việc này có thể gây ra lỗi XSS đột biến (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);
Phương án dự phòng và hỗ trợ trình duyệt cho API Trình dọn dẹp
Nếu API Trình dọn dẹp gốc không có trong trình duyệt mục tiêu, hãy chỉ tải một thư viện như DOMPurify khi thiếu tính năng hỗ trợ gốc để tránh gửi thư viện đó đến các trình duyệt không cần đến. Sử dụng các bước kiểm tra in để phát hiện tính năng, nhờ đó, bước kiểm tra này hoạt động trong các trình duyệt cũ.
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
});
}
}
Theo mặc định, DOMPurify giữ lại các thuộc tính aria-* và data-* (ngay cả khi ALLOWED_ATTR được chỉ định) cũng như nội dung văn bản của các phần tử mà nó xoá, trong khi Trình dọn dẹp gốc loại bỏ các thuộc tính aria-* và data-*, đồng thời xoá các phần tử không được phép cùng với nội dung của chúng. Việc đặt ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false và cho phép #text giúp DOMPurify khớp với hành vi gốc. DOMPurify không có tuỳ chọn tích hợp sẵn tương đương với replaceWithChildrenElements, vì vậy, hãy tránh tuỳ chọn đó trong các cấu hình cần có phương án dự phòng phù hợp.
Nếu trang thực thi Kiểu tin cậy (require-trusted-types-for 'script'), thì việc gán một chuỗi cho innerHTML sẽ gửi. setHTML() không cần chính sách Trusted Types, nhưng phương án dự phòng thì cần: truyền RETURN_TRUSTED_TYPE: true đến DOMPurify.sanitize() và cho phép tên chính sách dompurify trong lệnh trusted-types CSP.