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

API Trình dọn dẹp mới nhằm mục đích xây dựng một trình 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 việc hiển thị an toàn nội dung đó trong một 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 cơ hội cho các cuộc tấn công tập lệnh trên nhiều trang web (XSS) mà 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 xây dựng một bộ xử lý mạnh mẽ để chèn các chuỗi tuỳ ý vào một 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.

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

Tách biệt 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, bạn phải thoát các chuỗi đúng cách. Bạn cần đặc biệt chú ý đến việc thao tác DOM thông qua .innerHTML, trong đó các chuỗi chưa thoát là 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 HTML trong chuỗi đầu vào ở 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 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ữ lại 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à tránh thoát mà là làm sạch.

Xoá dữ liệu không hợp lệ do người dùng nhập

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

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

Việc dọn dẹp là để xoá các 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 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ý đó một cách an toàn trong DOM mà không làm ảnh hưởng đế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à có hại, đồng thời giữ lại các thẻ và thuộc tính không gây hại.

Thông số kỹ thuật API Trình dọn dẹp được đề xuất nhằm cung cấp quy trình xử lý đó dưới dạng API tiêu chuẩn cho trình duyệt.

API Trình dọn dẹp

Sanitizer API đượ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. Vì vậy, mã có thể giống như bên dưới.

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

Xin 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 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, API Trình dọn dẹp đượ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 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ử đã chỉ định.

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

blockElements: Tên của các phần tử mà trình dọn dẹp sẽ 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 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 dọn dẹp sẽ cho phép hay từ chối các thuộc tính đã chỉ định bằng các tuỳ chọn sau:

  • allowAttributes
  • dropAttributes

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ử 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>

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 API Trình dọn dẹp 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="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
/
/ `<em>hello world</em><img src="">`

DOMPurify có thể đóng vai trò 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 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ỉ nhận 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 tiế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 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 video 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 Ý định vận chuyển trên M105

Mozilla: Xem xét đề xuất này đáng để tạo bản minh hoạ và đang tích cực triển khai đề xuất này.

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

Cách bật API Trình dọn dẹp

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

Chrome

Chrome đang trong quá trình triển khai API Trình dọn dẹp. 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 thông qua --enable-blink-features=SanitizerAPI và dùng thử ngay. 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 dọn dẹp 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 được biết. Chia sẻ suy nghĩ của bạn về các vấn đề trên GitHub liên quan đến API Trình dọn dẹp và thảo luận với các tác giả 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 lỗi hoặc hành vi không mong muốn trong quá trình triển khai 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 API Trình dọn dẹp hoạt động như thế nào, hãy xem Sanitizer API Playground (Khu vui 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.