Manipulasi DOM aman dengan Sanitizer API

Sanitizer API baru bertujuan membangun prosesor yang tangguh untuk string arbitrer yang dapat disisipkan dengan aman ke halaman.

Jack J
Jack J

Aplikasi berurusan dengan string tidak tepercaya sepanjang waktu, tetapi merender konten tersebut dengan aman sebagai bagian dari dokumen HTML bisa jadi merepotkan. Tanpa kehati-hatian yang memadai, sangat mudah untuk secara tidak sengaja menciptakan peluang untuk pembuatan skrip lintas situs (XSS) yang dapat dieksploitasi oleh penyerang berbahaya.

Untuk mengurangi risiko tersebut, proposal Sanitizer API yang baru bertujuan untuk membuat pemroses yang andal agar string arbitrer dapat disisipkan dengan aman ke halaman. Artikel ini memperkenalkan API tersebut dan menjelaskan penggunaannya.

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

Meng-escape input pengguna

Saat menyisipkan input pengguna, string kueri, konten cookie, dan sebagainya, ke dalam DOM, string tersebut harus di-escape dengan benar. Perhatian khusus harus diberikan pada manipulasi DOM melalui .innerHTML, dengan string yang tidak di-escape merupakan sumber umum XSS.

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

Jika Anda meng-escape karakter khusus HTML dalam string input di atas atau meluaskannya menggunakan .textContent, alert(0) tidak akan dieksekusi. Namun, karena <em> yang ditambahkan oleh pengguna juga diperluas sebagai string seperti apa adanya, metode ini tidak dapat digunakan untuk mempertahankan dekorasi teks dalam HTML.

Hal terbaik yang harus dilakukan di sini adalah tidak mengonversi, tetapi membersihkan.

Membersihkan input pengguna

Perbedaan antara melarikan diri dan membersihkan

escape adalah penggantian karakter HTML khusus dengan Entitas HTML.

Sanitasi mengacu pada penghapusan bagian yang berbahaya secara semantik (seperti eksekusi skrip) dari string HTML.

Contoh

Pada contoh sebelumnya, <img onerror> menyebabkan pengendali error dieksekusi, tetapi jika pengendali onerror telah dihapus, akan dapat diperluas dengan aman di DOM tanpa mengubah <em>.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Untuk membersihkan dengan benar, Anda perlu mengurai string input sebagai HTML, menghilangkan tag dan atribut yang dianggap berbahaya, dan mempertahankan yang tidak berbahaya.

Spesifikasi Sanitizer API yang diusulkan bertujuan untuk menyediakan pemrosesan tersebut sebagai API standar untuk browser.

API Sanitizer

Sanitizer API digunakan dengan cara berikut:

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>

Namun, { sanitizer: new Sanitizer() } adalah argumen default. Jadi seperti di bawah ini.

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

Perlu diperhatikan bahwa setHTML() ditentukan di Element. Sebagai metode Element, konteks yang akan diurai cukup jelas (dalam hal ini <div>), penguraian dilakukan satu kali secara internal, dan hasilnya langsung diperluas ke DOM.

Untuk mendapatkan hasil sanitasi sebagai string, Anda dapat menggunakan .innerHTML dari hasil setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Menyesuaikan melalui konfigurasi

Sanitizer API dikonfigurasi secara default untuk menghapus string yang akan memicu eksekusi skrip. Namun, Anda juga dapat menambahkan penyesuaian sendiri ke proses sanitasi melalui objek konfigurasi.

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

Opsi berikut menentukan bagaimana hasil sanitasi harus memperlakukan elemen yang ditentukan.

allowElements: Nama elemen yang harus dipertahankan oleh pembersih.

blockElements: Nama elemen yang harus dihapus oleh pembersih, sambil mempertahankan turunannya.

dropElements: Nama elemen yang harus dihapus oleh pembersih, beserta turunannya.

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>

Anda juga dapat mengontrol apakah pembersih akan mengizinkan atau menolak atribut tertentu dengan opsi berikut:

  • allowAttributes
  • dropAttributes

Properti allowAttributes dan dropAttributes memerlukan daftar pencocokan atribut—objek yang kuncinya adalah nama atribut, dan nilainya adalah daftar elemen target atau karakter pengganti *.

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 adalah opsi untuk mengizinkan atau menolak elemen kustom. Jika diizinkan, konfigurasi lain untuk elemen dan atribut masih berlaku.

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>

Platform API

Perbandingan dengan DomPurify

DOMPurify adalah library terkenal yang menawarkan fungsi sanitasi. Perbedaan utama antara Sanitizer API dan DOMPurify adalah DOMPurify menampilkan hasil sanitasi sebagai string, yang perlu Anda tulis ke dalam elemen DOM melalui .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 dapat berfungsi sebagai penggantian saat Sanitizer API tidak diterapkan di browser.

Implementasi DOMPurify memiliki beberapa kelemahan. Jika string ditampilkan, string input akan diuraikan dua kali, oleh DOMPurify dan .innerHTML. Penguraian ganda ini memboroskan waktu pemrosesan, tetapi juga dapat menyebabkan kerentanan menarik yang disebabkan oleh kasus ketika hasil penguraian kedua berbeda dengan yang pertama.

HTML juga memerlukan konteks untuk diurai. Misalnya, <td> dapat diterima dalam <table>, tetapi tidak dalam <div>. Karena DOMPurify.sanitize() hanya mengambil string sebagai argumen, konteks penguraian harus ditebak.

Sanitizer API ditingkatkan berdasarkan pendekatan DOMPurify dan dirancang untuk menghilangkan kebutuhan akan penguraian ganda dan memperjelas konteks penguraian.

Status API dan dukungan browser

Sanitizer API sedang dibahas dalam proses standardisasi dan Chrome sedang dalam proses menerapkannya.

Langkah Status
1. Buat pesan penjelasan Selesai
2. Buat draf spesifikasi Selesai
3. Mengumpulkan masukan dan mengulangi desain Selesai
4. Uji coba origin Chrome Selesai
5. Peluncuran Rencana Pengiriman pada M105

Mozilla: Mempertimbangkan proposal ini layak dibuat prototipe, dan secara aktif menerapkannya.

WebKit: Lihat respons di milis WebKit.

Cara mengaktifkan Sanitizer API

Dukungan Browser

  • x
  • x
  • x

Sumber

Mengaktifkan melalui opsi about://flags atau CLI

Chrome

Chrome sedang dalam proses menerapkan Sanitizer API. Di Chrome 93 atau yang lebih baru, Anda dapat mencoba perilaku ini dengan mengaktifkan tanda about://flags/#enable-experimental-web-platform-features. Di versi Chrome Canary dan saluran Dev sebelumnya, Anda dapat mengaktifkannya melalui --enable-blink-features=SanitizerAPI dan mencobanya sekarang. Lihat petunjuk cara menjalankan Chrome dengan tanda.

Firefox

Firefox juga menerapkan Sanitizer API sebagai fitur eksperimental. Untuk mengaktifkannya, tetapkan tanda dom.security.sanitizer.enabled ke true di about:config.

Deteksi fitur

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

Masukan

Jika Anda mencoba API ini dan memiliki masukan, kami akan dengan senang hati mendengarnya. Bagikan pendapat Anda tentang masalah GitHub Sanitasizer API dan diskusikan dengan penulis spesifikasi dan orang yang tertarik dengan API ini.

Jika Anda menemukan bug atau perilaku yang tidak terduga dalam implementasi Chrome, laporkan bug untuk melaporkannya. Pilih komponen Blink>SecurityFeature>SanitizerAPI dan bagikan detail untuk membantu pengimplementasi melacak masalah.

Demo

Untuk melihat cara kerja Sanitizer API, lihat Sanitizer API Playground oleh Mike West:

Referensi


Foto oleh Towfiqu barbhuiya di Unsplash.