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 bộ xử lý mạnh mẽ để chèn các chuỗi tuỳ ý vào trang một cách an toàn.

Jack J
Jack J

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

Để giảm thiểu rủi ro đó, đề xuất mới về Sanitizer API sẽ hướng đến việc xây dựng một bộ xử lý mạnh mẽ để chèn các chuỗi tuỳ ý vào trang một cách an toàn. 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="" 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 điển hình của XSS.

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 HTML trong chuỗi nhập ở trên hoặc mở rộng chuỗi đó bằng cách sử dụng .textContent, alert(0) sẽ không được thực thi. Tuy nhiên, vì <em> được 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 để giữ phần trang trí văn bản trong HTML.

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

Dọn dẹp hoạt động đầu vào của người dùng

Sự khác biệt giữa việc thoát và vệ sinh

Thoát nghĩa 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á các phần có hại về mặt ngữ nghĩa (chẳng hạn như quá trình 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á, bạn có thể mở rộng trình xử lý này trong DOM một cách an toàn 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, 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à có hại, đồng thời giữ lại các thẻ và thuộc tính không gây hại.

Quy cách được đề xuất về Sanitizer API nhằm cung cấp quy trình xử lý như một API tiêu chuẩn cho các 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, nó có thể giống như bên dưới.

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

Điều đáng chú ý là 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à dễ hiểu (trong trường hợp này là <div>), việc phân tích cú pháp được thực hiện một lần nội bộ và kết quả được trực tiếp mở rộng sang DOM.

Để nhận kết quả dọn dẹp dưới dạng chuỗi, bạn có thể sử dụng .innerHTML trong 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 việc thực thi tập lệnh. Tuy nhiên, bạn cũng có thể thêm các yếu tố tuỳ chỉnh của riêng mình vào quy trình dọn dẹp thông qua đố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 xử lý phần tử được chỉ định.

allowElements: Tên của các thành phần mà chất khử trùng nên giữ lại.

blockElements: Tên của các thành phần mà chất vệ sinh cầ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 thành phần mà chất vệ sinh cần loại bỏ cùng với các thành phần 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 xem 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

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 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 })
// <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ả 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 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 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 kép 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 thú vị gây ra bởi trường hợp 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 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 theo phương pháp DOMPurify và được thiết kế để bạn không cần phải 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 quá trình tiêu chuẩn hoá và Chrome đang trong quá trình triển khai.

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 quy cách 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. Launch Ý định vận chuyển trên M105

Mozilla: Cân nhắc đề xuất này đáng để thử nghiệm và đ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

  • x
  • x
  • x

Nguồn

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

Chrome

Chrome đang triển khai API Sanitizer. Trong Chrome 93 trở lên, bạn có thể 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 nhà phát triển, 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 về cách chạy Chrome có cờ.

Firefox

Firefox cũng triển khai API Sanitizer dưới dạng tính năng thử nghiệm. Để bật chế độ 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
}

Ý kiến 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 phản hồi. Chia sẻ ý kiến của bạn về các vấn đề liên quan đến API Sanitizer trên GitHub, đồng thời thảo luận với tác giả và những người quan tâm đến API này.

Nếu bạn tìm thấy 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 sự cố đó. 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 cách API Sanitizer hoạt động, 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.