HTML bermasalah di Async Clipboard API

Mulai Chrome 120, opsi unsanitized baru tersedia di Async Clipboard API. Opsi ini dapat membantu dalam situasi khusus terkait HTML, saat Anda perlu menempelkan konten papan klip seperti saat disalin. Artinya, tanpa langkah pembersihan perantara yang biasanya diterapkan browser—dan dengan alasan yang baik. Pelajari cara menggunakannya dalam panduan ini.

Saat menggunakan Async Clipboard API, dalam sebagian besar kasus, developer tidak perlu khawatir dengan integritas konten di papan klip dan dapat mengasumsikan bahwa apa yang mereka tulis ke papan klip (salin) sama dengan yang akan mereka dapatkan saat membaca data dari papan klip (tempel).

Hal ini berlaku untuk teks. Coba tempel kode berikut di Konsol DevTools, lalu segera fokuskan kembali halaman. (setTimeout() diperlukan agar Anda memiliki cukup waktu untuk memfokuskan halaman, yang merupakan persyaratan Async Clipboard API.) Seperti yang Anda lihat, input sama persis dengan output.

setTimeout(async () => {
  const input = 'Hello';
  await navigator.clipboard.writeText(input);
  const output = await navigator.clipboard.readText();
  console.log(input, output, input === output);
  // Logs "Hello Hello true".
}, 3000);

Untuk gambar, prosesnya sedikit berbeda. Untuk mencegah apa yang disebut serangan bom kompresi, browser mengenkode ulang gambar seperti PNG, tetapi gambar input dan output secara visual sama persis, piksel per piksel.

setTimeout(async () => {
  const dataURL =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=';
  const input = await fetch(dataURL).then((response) => response.blob());
  await navigator.clipboard.write([
    new ClipboardItem({
      [input.type]: input,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const output = await clipboardItem.getType(input.type);
  console.log(input.size, output.size, input.type === output.type);
  // Logs "68 161 true".
}, 3000);

Namun, apa yang terjadi dengan teks HTML? Seperti yang Anda duga, dengan HTML, situasinya berbeda. Di sini, browser membersihkan kode HTML untuk mencegah hal buruk terjadi, misalnya, menghapus tag <script> dari kode HTML (dan lainnya seperti <meta>, <head>, dan <style>) dan dengan menyisipkan CSS. Perhatikan contoh berikut dan coba di Konsol DevTools. Anda akan melihat bahwa output sangat berbeda dengan inputnya.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

Pembersihan HTML umumnya merupakan hal yang baik. Anda tentu tidak ingin membuat diri Anda terpapar masalah keamanan dengan mengizinkan HTML yang bermasalah dalam sebagian besar kasus. Namun, ada skenario saat developer tahu persis apa yang mereka lakukan dan integritas HTML input dan output sangat penting untuk fungsi aplikasi yang benar. Dalam situasi ini, Anda memiliki dua pilihan:

  1. Jika Anda mengontrol akhir penyalinan dan penempelan, misalnya, jika Anda menyalin dari dalam aplikasi, lalu menempelkan dalam aplikasi, Anda harus menggunakan Format kustom web untuk Async Clipboard API. Berhenti membaca di sini dan periksa artikel tertaut.
  2. Jika Anda hanya mengontrol akhir proses menempel di aplikasi, tetapi tidak mengontrol akhir proses menyalin, mungkin karena operasi salin terjadi di aplikasi native yang tidak mendukung format kustom web, Anda harus menggunakan opsi unsanitized, yang dijelaskan di bagian lain artikel ini.

Sanitasi mencakup hal-hal seperti menghapus tag script, menyisipkan gaya, dan memastikan HTML tersusun dengan baik. Daftar ini tidak lengkap, dan lebih banyak langkah dapat ditambahkan di masa mendatang.

Salin dan tempel HTML yang bermasalah

Saat Anda write() (menyalin) HTML ke papan klip dengan Async Clipboard API, browser memastikan bahwa HTML tersebut terbentuk dengan baik dengan menjalankannya melalui parser DOM dan melakukan serialisasi string HTML yang dihasilkan, tetapi tidak ada pembersihan yang terjadi pada langkah ini. Anda tidak perlu melakukan apa pun. Jika Anda read() HTML yang ditempatkan di papan klip oleh aplikasi lain, dan aplikasi web Anda memilih untuk mendapatkan konten fidelitas lengkap dan perlu melakukan pembersihan dalam kode Anda sendiri, Anda dapat meneruskan objek opsi ke metode read() dengan properti unsanitized dan nilai ['text/html']. Secara terpisah, tampilannya seperti ini: navigator.clipboard.read({ unsanitized: ['text/html'] }). Contoh kode berikut di bawah hampir sama dengan yang ditampilkan sebelumnya, tetapi kali ini dengan opsi unsanitized. Saat mencobanya di DevTools Console, Anda akan melihat bahwa input dan output-nya sama.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html'],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

Dukungan browser dan deteksi fitur

Tidak ada cara langsung untuk memeriksa apakah fitur didukung, sehingga deteksi fitur didasarkan pada pengamatan perilaku. Oleh karena itu, contoh berikut bergantung pada deteksi fakta apakah tag <style> tetap ada, yang menunjukkan dukungan, atau di-inline, yang menunjukkan tidak adanya dukungan. Perhatikan bahwa agar hal ini berfungsi, halaman sudah harus mendapatkan izin papan klip.

const supportsUnsanitized = async () => {
  const input = `<style>p{color:red}</style><p>a`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  return /<style>/.test(output);
};

Demo

Untuk melihat cara kerja opsi unsanitized, lihat demo di Glitch dan lihat kode sumbernya.

Kesimpulan

Seperti yang diuraikan dalam pengantar, sebagian besar developer tidak perlu mengkhawatirkan sanitasi papan klip dan hanya perlu menangani pilihan sanitasi default yang dibuat oleh browser. Untuk kasus yang jarang terjadi saat developer perlu memperhatikan, opsi unsanitized ada.

Ucapan terima kasih

Artikel ini ditinjau oleh Anupam Snigdha dan Rachel Andrew. API ini ditentukan dan diimplementasikan oleh tim Microsoft Edge.