Thao tác DOM an toàn bằng Sanitizer API

Sanitizer API mới hướng đến việc xây dựng một trình xử lý mạnh mẽ cho các chuỗi tuỳ ý để được chèn một cách an toàn vào một trang.

Jack J
Jack J

Các ứng dụng luôn phải xử lý các chuỗi không đáng tin cậy, nhưng việc hiển thị nội dung đó một cách an toàn trong tài liệu HTML có thể rất khó khăn. Nếu không cẩn thận, bạn có thể vô tình tạo ra cơ hội cho tập lệnh trên nhiều trang web (XSS) mà những kẻ tấn công độc hại có thể khai thác.

Để giảm thiểu rủi ro đó, đề xuất Sanitizer API mới nhằm mục đích xây dựng một bộ xử lý mạnh mẽ cho các chuỗi tuỳ ý để được chèn an toàn vào một trang. Bài viết này giới thiệu về API và giải thích cách sử dụng API này.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())

Thoát khỏi dữ liệu đầu vào của người dùng

Khi chèn dữ liệu đầu vào của người dùng, chuỗi truy vấn, nội dung cookie, v.v. vào DOM, các chuỗi phải được thoát đúng cách. Bạn nên đặc biệt chú ý đến việc thao tác DOM thông qua .innerHTML, trong đó các chuỗi chưa được thoát là nguồn gốc điển hình của XSS.

const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
$div.innerHTML = user_input

Nếu bạn thoát các ký tự đặc biệt trong HTML ở chuỗi đầu vào bên trên hoặc mở rộng chuỗi đó bằng cách sử dụng .textContent, thì alert(0) sẽ không được thực thi. Tuy nhiên, vì <em> do người dùng thêm cũng được mở rộng dưới dạng một chuỗi, nên bạn không thể dùng phương thức này để giữ phần trang trí văn bản trong HTML.

Điều tốt nhất cần làm ở đây không phải là thoát mà là dọn dẹp.

Làm sạch dữ liệu đầu vào của người dùng

Sự khác biệt giữa việc thoát và dọn dẹp

Thoát đề cập đến việc thay thế các ký tự HTML đặc biệt bằng Thực thể HTML.

Thanh lọc là quá trình xoá các phần có hại về mặt ngữ nghĩa (chẳng hạn như việc thực thi tập lệnh) khỏi các chuỗi HTML.

Ví dụ:

Trong ví dụ trước, <img onerror> khiến trình xử lý lỗi được thực thi, nhưng nếu trình xử lý onerror bị xoá, thì bạn có thể mở rộng trình xử lý này một cách an toàn trong DOM mà vẫn giữ nguyên <em>.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`

Để dọn dẹp đúng cách, bạn cần phân tích cú pháp chuỗi đầu vào dưới dạng HTML, bỏ qua các thẻ và thuộc tính bị coi là có hại, đồng thời giữ lại những thẻ và thuộc tính vô hại.

Đề xuất về quy cách Sanitizer API nhằm cung cấp quy trình xử lý như vậy dưới dạng một API tiêu chuẩn cho trình duyệt.

Sanitizer API

Sanitizer API được dùng theo cách sau:

const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div

Tuy nhiên, { sanitizer: new Sanitizer() } là đối số mặc định. Vì vậy, bạn có thể sử dụng mã như bên dưới.

$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>&quot;/div

Bạn nên lưu ý rằng setHTML() được xác định trên Element. Là một phương thức của Element, ngữ cảnh để phân tích cú pháp là tự giải thích (<div> trong trường hợp này), quá trình phân tích cú pháp được thực hiện một lần nội bộ và kết quả được mở rộng trực tiếp vào DOM.

Để nhận kết quả của quá trình dọn dẹp dưới dạng một chuỗi, bạn có thể sử dụng .innerHTML từ kết quả setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g src=""

Tuỳ chỉnh thông qua cấu hình

Theo mặc định, Sanitizer API được định cấu hình để xoá các chuỗi có thể kích hoạt quá trình thực thi tập lệnh. Tuy nhiên, bạn cũng có thể thêm các chế độ tuỳ chỉnh của riêng mình vào quy trình dọn dẹp thông qua một đối tượng cấu hình.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

Các lựa chọn sau đây chỉ định cách kết quả dọn dẹp sẽ xử lý phần tử được chỉ định.

allowElements: Tên của các phần tử mà trình dọn dẹp cần giữ lại.

blockElements: Tên của các phần tử mà trình dọn dẹp cần xoá, trong khi vẫn giữ lại các phần tử con.

dropElements: Tên của các phần tử mà trình dọn dẹp cần xoá, cùng với các phần tử con của chúng.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" <]})> })
//< >divhe<ll><o bw>orld/b/div

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div

Bạn cũng có thể kiểm soát việc trình dọn dẹp cho phép hay từ chối các thuộc tính được chỉ định bằng các lựa chọn sau:

  • allowAttributes
  • dropAttributes

Các thuộc tính allowAttributesdropAttributes dự kiến sẽ có danh sách so khớp thuộc tính – các đối tượng có khoá là tên thuộc tính và giá trị là danh sách các phần tử đích hoặc ký tự đại diện *.

const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`

$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div

$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<

$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div

$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/div

allowCustomElements là lựa chọn cho phép hoặc từ chối các phần tử tuỳ chỉnh. Nếu được phép, các cấu hình khác cho phần tử và thuộc tính vẫn được áp dụng.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str<, {>< sanitizer >})
//< divcustom-e><lemh>ello/custom-elem/div

Nền tảng API

So sánh với DomPurify

DOMPurify là một thư viện nổi tiếng cung cấp chức năng dọn dẹp. Điểm khác biệt chính giữa Sanitizer API và DOMPurify là DOMPurify trả về kết quả của quá trình dọn dẹp dưới dạng một chuỗi mà bạn cần ghi vào một phần tử DOM thông qua .innerHTML.

const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o world/em>img src=""`

DOMPurify có thể đóng vai trò là giải pháp dự phòng khi Sanitizer API không được triển khai trong trình duyệt.

Việc triển khai DOMPurify có một số nhược điểm. Nếu một chuỗi được trả về, thì chuỗi đầu vào sẽ được phân tích cú pháp hai lần, bằng DOMPurify và .innerHTML. Việc phân tích cú pháp hai lần này làm lãng phí thời gian xử lý, nhưng cũng có thể dẫn đến các lỗ hổng bảo mật thú vị do các trường hợp mà kết quả của lần phân tích cú pháp thứ hai khác với lần đầu tiên.

HTML cũng cần context để được phân tích cú pháp. Ví dụ: <td> có ý nghĩa trong <table> nhưng không có ý nghĩa trong <div>. Vì DOMPurify.sanitize() chỉ lấy một chuỗi làm đối số, nên ngữ cảnh phân tích cú pháp phải được đoán.

Sanitizer API cải thiện phương pháp DOMPurify và được thiết kế để loại bỏ nhu cầu phân tích cú pháp hai lần cũng như làm rõ ngữ cảnh phân tích cú pháp.

Trạng thái API và khả năng hỗ trợ trình duyệt

Sanitizer API đang được thảo luận trong quy trình chuẩn hoá và Chrome đang trong quá trình triển khai API này.

Bước Trạng thái
1. Tạo video giải thích Hoàn tất
2. Tạo bản nháp quy cách Hoàn tất
3. Thu thập ý kiến phản hồi và lặp lại quy trình thiết kế Hoàn tất
4. Bản dùng thử theo nguyên gốc trên Chrome Hoàn tất
5. Khởi chạy Dự kiến giao hàng vào M105

Mozilla: Cho rằng đề xuất này đáng để tạo mẫuđang tích cực triển khai đề xuất này.

WebKit: Xem câu trả lời trên danh sách gửi thư của WebKit.

Cách bật Sanitizer API

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

Bật thông qua tuỳ chọn about://flags hoặc CLI

Chrome

Chrome đang trong quá trình triển khai Sanitizer API. Trong Chrome 93 trở lên, bạn có thể dùng thử hành vi này bằng cách bật cờ about://flags/#enable-experimental-web-platform-features. Trong các phiên bản trước của Chrome Canary và Kênh dành cho nhà phát triển, bạn có thể bật tính năng này thông qua --enable-blink-features=SanitizerAPI và dùng thử ngay bây giờ. Hãy xem hướng dẫn về cách chạy Chrome bằng các cờ.

Firefox

Firefox cũng triển khai Sanitizer API dưới dạng một tính năng thử nghiệm. Để bật tính năng này, hãy đặt cờ dom.security.sanitizer.enabled thành true trong about:config.

Phát hiện đối tượng

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Phản hồi

Nếu bạn dùng thử API này và có ý kiến phản hồi, chúng tôi rất mong nhận được ý kiến của bạn. Chia sẻ ý kiến của bạn về các vấn đề về Sanitizer API trên GitHub và thảo luận với các tác giả của thông số kỹ thuật cũng như những người quan tâm đến API này.

Nếu bạn phát hiện thấy lỗi hoặc hành vi không mong muốn trong quá trình triển khai của Chrome, hãy gửi báo cáo lỗi để thông báo về vấn đề này. Chọn các thành phần Blink>SecurityFeature>SanitizerAPI và chia sẻ thông tin chi tiết để giúp người triển khai theo dõi vấn đề.

Bản minh hoạ

Để xem API Trình dọn dẹp hoạt động, hãy xem Sân chơi API Trình dọn dẹp của Mike West:

Tài liệu tham khảo


Ảnh chụp của Towfiqu barbhuiya trên Unsplash.