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

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 kết xuất 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 có thể rất khó. Nếu không cẩn thận, bạn có thể vô tình tạo 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 API Trình làm sạch mới nhằm xây dựng một bộ xử lý mạnh mẽ cho các chuỗi tuỳ ý để chèn an toàn vào một trang.

// 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 nên đặc biệt chú ý đến việc thao tác DOM bằng .innerHTML, trong đó các chuỗi không được thoát là nguồn gốc đ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 của HTML trong chuỗi đầu vào 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 cũng được mở rộng dưới dạng một chuỗi, nên bạn không thể sử dụng phương thức này để giữ nguyên kiểu 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à là làm sạch.

Làm sạch hoạt động đầu vào của người dùng

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

Làm sạch là loại bỏ các phần có hại về mặt ngữ nghĩa (chẳng hạn 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á, thì bạn có thể mở rộng trình xử lý đó một cách an toàn trong DOM trong khi 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="">`

Để làm sạch đú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à có hại, đồng thời giữ lại những thẻ và thuộc tính vô hại.

Thông số kỹ thuật API Trình làm sạch được đề xuất 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.

API Trình làm sạch

API Trình làm sạch được sử dụng 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.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></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 ở bên trong và kết quả được mở rộng trực tiếp vào DOM.

Để nhận kết quả làm sạch 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.innerHTML // <em>hello world</em><img src="">

Tuỳ chỉnh bằng cấu hình

API Trình làm sạch được định cấu hình theo mặc đị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 tuỳ chỉnh của riêng mình vào quy trình làm sạch bằng 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ả làm sạch sẽ xử lý phần tử được chỉ định.

allowElements: Tên của các phần tử mà trình làm sạch sẽ giữ lại.

blockElements: Tên của các phần tử mà trình làm sạch sẽ xoá, đồng thời giữ lại các phần tử con của chúng.

dropElements: Tên của các phần tử mà trình làm sạch sẽ 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" ]}) })
// <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 làm sạch 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 mong đợi 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ử 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à tuỳ 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 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>

Bề mặt 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 làm sạch. Điểm khác biệt chính giữa API Trình làm sạch và DOMPurify là DOMPurify trả về kết quả làm sạch dưới dạng một chuỗi mà bạn cần ghi vào một phần tử DOM bằng .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 API Trình làm sạch không được triển khai trong trình duyệt.

Việc triển khai DOMPurify có một vài 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ởi 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ị do các 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 ngữ cảnh phân tích cú pháp phải được đoán.

API Trình làm sạch 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 kép và 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

API Trình làm sạch đang được thảo luận trong quá 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 phần 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à lặp lại thiết kế Hoàn tất
4. Bản dùng thử theo nguyên gốc của Chrome Hoàn tất
5. Khởi chạy Dự định phát hành trên M105

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

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

Cách bật API Trình làm sạch

Browser Support

  • Chrome: 146.
  • Edge: 146.
  • Firefox: 148.
  • Safari: not supported.

Chrome đang trong quá trình triển khai API Trình làm sạch. 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 nhà phát triển, bạn có thể bật cờ này bằng --enable-blink-features=SanitizerAPI. Hãy xem hướng dẫn về cách chạy Chrome bằng cờ.

Firefox

Firefox cũng triển khai API Trình làm sạch 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 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 của bạn. Chia sẻ ý kiến của bạn về các vấn đề trên GitHub của API Trình làm sạch và thảo luận với tác giả thông số kỹ thuật và 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 để 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 API Trình làm sạch hoạt động, hãy xem Sân chơi API Trình làm sạch của Mike West:

Tài liệu tham khảo