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

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

Jack J
Jack J

Ứng dụng luôn xử lý các chuỗi không đáng tin cậy, nhưng việc hiển thị một cách an toàn nội dung đó dưới dạng một phần tài liệu HTML có thể là một công việc khó khăn. Nếu không cẩn thận, những kẻ tấn công có thể rất dễ vô tình tạo ra cơ hội Cross-site scripting (XSS) cho những kẻ tấn công.

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

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

Thoát hoạt động đầu vào của người dùng

Khi chèn hoạt động đầ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 cần đặc biệt chú ý đến thao tác DOM thông qua .innerHTML, trong đó các chuỗi không thoát là một nguồn XSS điển hình.

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

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

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

Dọn dẹp dữ liệu do người dùng nhập

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

Thoát là thay thế các ký tự HTML đặc biệt bằng Thực thể HTML.

Dọn dẹp là việc xoá những phần gây hại về mặt ngữ nghĩa (chẳng hạn như thực thi tập lệnh) khỏi 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á, 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="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello 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 được coi là gây hại và giữ lại những thẻ và thuộc tính vô hại.

Quy cách của API Sanitizer được đề xuất nhằm mục đích cung cấp cách xử lý như API tiêu chuẩn cho trình duyệt.

API Sanitizer

Bạn có thể sử dụng Sanitizer API theo cách sau:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

Tuy nhiên, { sanitizer: new Sanitizer() } là đối số mặc định. Vì vậy, sự kiện có thể giống như dưới đây.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

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

Để nhận được kết quả dọn dẹp dưới dạng 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.innerHTML // <em>hello world</em><img 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 sẽ 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 quá 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 tuỳ 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 thành phần mà dung dịch vệ sinh nên loại bỏ trong khi vẫn giữ lại các thành phần con.

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

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" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

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

  • allowAttributes
  • dropAttributes

Các thuộc tính allowAttributesdropAttributes yêu cầu 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 phần tử mục tiêu hoặc ký tự đại diện *.

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

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</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 cho phép, các cấu hình khác cho các 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 })
// <div><custom-elem>hello</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ả dọn dẹp dưới dạng chuỗi mà bạn cần ghi vào phần tử DOM qua .innerHTML.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify có thể đóng vai trò là phương án dự phòng khi không triển khai Sanitizer API 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 đáng chú ý do các trường hợp mà kết quả của lần phân tích cú pháp lần thứ hai khác với lần đầu tiên.

HTML cũng cần có ngữ cảnh để đượ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 bạn phải đoán ngữ cảnh phân tích cú pháp.

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à hỗ trợ trình duyệt

Sanitizer API đang được thảo luận trong quá trình tiêu 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 thông báo giải thích Hoàn tất
2. Tạo bản nháp thông số kỹ thuật Hoàn tất
3. Thu thập ý kiến phản hồi và cải tiến thiết kế Hoàn tất
4. Bản dùng thử theo nguyên gốc Chrome Hoàn tất
5. Khởi chạy Ý định xuất hàng trên M105

Mozilla: Xem xét đề xuất này đáng để tạo nguyên mẫuđang tích cực triển khai.

WebKit: Xem phản hồi trên danh sách gửi thư WebKit.

Cách bật Sanitizer API

Hỗ trợ trình duyệt

  • Chrome: không được hỗ trợ.
  • Edge: không được hỗ trợ.
  • Firefox: sau một lá cờ.
  • Safari: không được hỗ trợ.

Nguồn

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ể thử hành vi bằng cách bật cờ about://flags/#enable-experimental-web-platform-features. Trong các phiên bản trước đây của Chrome Canary và kênh Dev, bạn có thể bật tính năng này qua --enable-blink-features=SanitizerAPI và dùng thử ngay bây giờ. Hãy xem hướng dẫn cách chạy Chrome có cờ.

Firefox

Firefox cũng triển khai Sanitizer API dưới dạng 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 tính nă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 phản hồi của bạn. Hãy chia sẻ ý kiến của bạn về các vấn đề về API của Sanitizer trên GitHub và thảo luận với các tác giả cũng như những người quan tâm đến API này.

Nếu bạn phát hiện bất kỳ lỗi hoặc hành vi không mong muốn nào trong quá trình triển khai của Chrome, hãy gửi lỗi để báo cáo. 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 Sanitizer API hoạt động như thế nào trong thực tế, hãy xem Sanitizer API Playground của Mike West:

Tài liệu tham khảo


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