Manipulasi DOM aman dengan Sanitizer API

Sanitizer API baru bertujuan untuk membuat pemroses yang andal untuk string arbitrer agar dapat disisipkan dengan aman ke dalam halaman.

Jack J
Jack J

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

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

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

Meng-escape input pengguna

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

const user_input = `<em>hello world</em><img src="" onerro>r=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 apa adanya, metode ini tidak dapat digunakan untuk mempertahankan dekorasi teks di HTML.

Hal terbaik yang harus dilakukan di sini bukanlah meng-escape, melainkan membersihkan.

Membersihkan input pengguna

Perbedaan antara escaping dan sanitasi

Escape mengacu pada penggantian karakter HTML khusus dengan Entity 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 dihapus, pengendali tersebut dapat diperluas dengan aman di DOM sambil membiarkan <em> tetap utuh.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`

Untuk melakukan sanitasi dengan benar, string input harus diuraikan sebagai HTML, tag dan atribut yang dianggap berbahaya harus dihilangkan, dan tag serta atribut yang tidak berbahaya harus dipertahankan.

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

Sanitizer API

Sanitizer API digunakan dengan cara berikut:

const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div

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

$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>&quot;/div

Perlu diperhatikan bahwa setHTML() ditentukan pada Element. Sebagai metode Element, konteks yang akan diuraikan sudah jelas (<div> dalam hal ini), penguraian dilakukan sekali secara internal, dan hasilnya langsung diperluas ke dalam DOM.

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

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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 pembersihan 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 cara hasil pembersihan harus memperlakukan elemen yang ditentukan.

allowElements: Nama elemen yang harus dipertahankan oleh sanitizer.

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

dropElements: Nama elemen yang harus dihapus oleh sanitizer, 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" <]})> })
//< >divhe<ll><o bw>orld/b/div

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div

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

  • allowAttributes
  • dropAttributes

Properti allowAttributes dan dropAttributes mengharapkan daftar kecocokan 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&<quot;>hello/span`

$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div

$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<

$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div

$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/div

allowCustomElements adalah opsi untuk mengizinkan atau menolak elemen kustom. Jika diizinkan, konfigurasi lain untuk elemen dan atribut tetap 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 >})
//< divcustom-e><lemh>ello/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="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o world/em>img src=""`

DOMPurify dapat berfungsi sebagai penggantian jika Sanitizer API tidak diterapkan di browser.

Penerapan DOMPurify memiliki beberapa kekurangan. Jika string ditampilkan, string input akan diurai dua kali, oleh DOMPurify dan .innerHTML. Penguraian ganda ini membuang waktu pemrosesan, tetapi juga dapat menyebabkan kerentanan menarik yang disebabkan oleh kasus ketika hasil penguraian kedua berbeda dari yang pertama.

HTML juga memerlukan konteks untuk diuraikan. Misalnya, <td> masuk akal di <table>, tetapi tidak di <div>. Karena DOMPurify.sanitize() hanya menggunakan string sebagai argumen, konteks parsing harus ditebak.

Sanitizer API meningkatkan pendekatan DOMPurify dan dirancang untuk menghilangkan kebutuhan akan parsing ganda dan memperjelas konteks parsing.

Status API dan dukungan browser

Sanitizer API sedang dibahas dalam proses standarisasi dan Chrome sedang dalam proses penerapannya.

Langkah Status
1. Membuat penjelasan Selesai
2. Buat draf spesifikasi Selesai
3. Mengumpulkan masukan dan melakukan iterasi pada desain Selesai
4. Uji coba origin Chrome Selesai
5. Luncurkan Maksud Pengiriman di M105

Mozilla: Menganggap proposal ini layak dibuat prototipenya, dan sedang mengimplementasikannya secara aktif.

WebKit: Lihat respons di daftar milis WebKit.

Cara mengaktifkan Sanitizer API

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

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 sebelumnya dari saluran Dev dan Chrome Canary, Anda dapat mengaktifkannya melalui --enable-blink-features=SanitizerAPI dan mencobanya sekarang. Lihat petunjuk tentang cara menjalankan Chrome dengan tanda.

Firefox

Firefox juga menerapkan Sanitizer API sebagai fitur eksperimental. Untuk mengaktifkannya, setel 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 ingin mendengarnya. Sampaikan pendapat Anda tentang masalah GitHub Sanitizer API dan diskusikan dengan penulis spesifikasi dan orang-orang yang tertarik dengan API ini.

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

Demo

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

Referensi


Foto oleh Towfiqu barbhuiya di Unsplash.