Membersihkan HTML yang Tidak Tepercaya

Fitur web: Sanitizer API

Menampilkan HTML buatan pengguna dengan aman adalah tantangan keamanan yang umum. Sanitizer API native menyediakan cara bawaan yang dioptimalkan browser untuk menghapus konten berbahaya (seperti tag <script> atau atribut on*) sebelum mencapai DOM.

Sanitasi sisi klien melindungi halaman yang merender konten. Jika konten buatan pengguna yang sama juga dirender di tempat lain (klien lain, halaman yang dirender server, atau email), bersihkan konten tersebut di server serta pertahanan mendalam, dan pastikan aturan pembersihan server dan klien tidak bertentangan.

1. Menyisipkan HTML dengan Aman menggunakan setHTML()

Cara utama untuk menggunakan Sanitizer API adalah melalui metode setHTML() di Element mana pun. Metode ini menangani penguraian, sanitasi, dan penyisipan dalam satu langkah, sehingga memastikan bahwa konten dibersihkan secara khusus untuk konteks tempat konten tersebut akan disisipkan.

const untrustedHTML = `
  <p>Hello!</p>
  <script>alert('XSS')</script>
  <img src="x" onerror="alert('XSS')">
  <div onclick="doBadThing()">Click me</div>
`;

const container = document.getElementById('output');

// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);

2. Menggunakan Konfigurasi Sanitizer Kustom

Anda dapat menyesuaikan aturan sanitasi dengan membuat instance Sanitizer. Dengan demikian, Anda dapat menentukan dengan tepat elemen dan atribut mana yang diizinkan untuk kasus penggunaan spesifik Anda.

Saat menggunakan Element.setHTML() dan Document.parseHTML(), setelan Sanitizer default akan menghapus semua elemen dan atribut yang tidak aman dari XSS. Anda tidak dapat membuat Sanitizer yang lebih permisif yang mengizinkan elemen tersebut, tetapi Anda dapat lebih membatasi apa yang diizinkan.

// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
  elements: ["p", "b", "i", "strong", "em"],
  attributes: ["class"],
  replaceWithChildrenElements: ['div']
};

const mySanitizer = new Sanitizer(config);

// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });

Elemen yang tidak ada di elements akan dihapus bersama dengan kontennya. Cantumkan elemen wrapper yang teksnya ingin Anda pertahankan (seperti div di atas) di replaceWithChildrenElements, yang akan menghapus elemen, tetapi mempertahankan turunannya.

Jika Anda memerlukan Sanitizer yang lebih permisif yang mengizinkan beberapa elemen atau atribut yang tidak aman dari XSS, Anda dapat menggunakan Sanitizer kustom dengan Element.setHTMLUnsafe() dan Document.parseHTMLUnsafe().

3. Mengurai Input Pengguna dengan Document.parseHTML()

Jika Anda perlu mengurai HTML yang tidak tepercaya tanpa langsung menyisipkannya ke DOM aktif, gunakan Document.parseHTML(). Metode ini menampilkan Document yang telah dibersihkan sesuai dengan aturan default atau kustom, sehingga Anda dapat memeriksa atau mengubah node yang telah dibersihkan (misalnya, untuk membuat pratinjau atau mengekstrak teks) sebelum menyisipkannya.

Pindahkan node yang dihasilkan ke halaman, bukan melakukan serialisasi ke string dan mengurainya kembali dengan innerHTML, yang dapat memunculkan kembali mutasi XSS (mXSS).

const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';

const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);

Penggantian & dukungan browser untuk Sanitizer API

Jika Sanitizer API native tidak tersedia di browser target Anda, muat library seperti DOMPurify hanya jika dukungan native tidak ada, untuk menghindari pengiriman ke browser yang tidak memerlukannya. Gunakan pemeriksaan in untuk deteksi fitur sehingga pemeriksaan itu sendiri berfungsi di browser lama.

let domPurifyPromise;

function loadDOMPurify() {
  // Code-split with your bundler so DOMPurify is only downloaded when needed
  domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
  return domPurifyPromise;
}

export async function safeSetHTML(el, html) {
  const elements = ['p', 'b', 'i', 'strong', 'em'];
  const attributes = ['class'];

  if ('setHTML' in Element.prototype) {
    // setHTML() accepts a plain configuration object as the sanitizer
    el.setHTML(html, { sanitizer: { elements, attributes } });
  } else {
    const DOMPurify = await loadDOMPurify();
    el.innerHTML = DOMPurify.sanitize(html, {
      ALLOWED_TAGS: [...elements, '#text'],
      ALLOWED_ATTR: attributes,
      // Match the native Sanitizer: strip aria-*/data-* and disallowed element content
      ALLOW_ARIA_ATTR: false,
      ALLOW_DATA_ATTR: false,
      KEEP_CONTENT: false
    });
  }
}

Secara default, DOMPurify mempertahankan atribut aria-* dan data-* (bahkan saat ALLOWED_ATTR ditentukan) serta konten teks elemen yang dihapusnya, sedangkan Sanitizer native menghapus atribut aria-* dan data-* serta menghapus elemen yang tidak diizinkan beserta kontennya. Menetapkan ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false, dan mengizinkan #text akan membuat DOMPurify cocok dengan perilaku native. DOMPurify tidak memiliki opsi bawaan yang setara dengan replaceWithChildrenElements, jadi hindari opsi tersebut dalam konfigurasi yang memerlukan penggantian yang cocok.

Jika halaman menerapkan Trusted Types (require-trusted-types-for 'script'), penetapan string ke innerHTML akan menampilkan error. setHTML() tidak memerlukan kebijakan Trusted Types, tetapi penggantinya memerlukan: teruskan RETURN_TRUSTED_TYPE: true ke DOMPurify.sanitize() dan izinkan nama kebijakan dompurify-nya di perintah CSP trusted-types Anda.