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, yang mengharuskan Anda menempelkan konten papan klip yang sama persis dengan konten saat disalin. Artinya, tanpa langkah sanitasi menengah yang umumnya diterapkan browser dan untuk alasan yang baik. Pelajari cara menggunakannya dalam panduan ini.

Saat menggunakan Async Clipboard API, pada sebagian besar kasus, developer tidak perlu mengkhawatirkan integritas konten di papan klip dan dapat menganggap bahwa apa yang mereka tulis ke papan klip (salinan) adalah sama dengan yang akan didapatkan saat membaca data dari papan klip (menempel).

Hal ini jelas berlaku untuk teks. Coba tempelkan 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 persis sama 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);

Dengan gambar, terlihat 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);

Bagaimanapun, apa yang terjadi dengan teks HTML? Seperti yang Anda duga, dengan HTML, situasinya berbeda. Di sini, browser membersihkan kode HTML untuk mencegah hal-hal buruk terjadi, misalnya dengan menghapus tag <script> dari kode HTML (dan lainnya seperti <meta>, <head>, dan <style>) dan dengan membuat CSS menjadi inline. Pertimbangkan contoh berikut dan cobalah di DevTools Console. Anda akan melihat bahwa output sangat berbeda dengan input.

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);

Sanitasi HTML umumnya adalah hal yang baik. Anda tentu tidak ingin menempatkan diri Anda pada masalah keamanan dengan mengizinkan HTML yang tidak bersih dalam kebanyakan kasus. Namun, ada beberapa skenario saat developer tahu persis apa yang mereka lakukan dan di mana integritas HTML dalam dan output sangat penting untuk fungsi aplikasi yang benar. Dalam situasi ini, Anda memiliki dua pilihan:

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

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

Salin dan tempel HTML yang bermasalah

Saat Anda write() (menyalin) HTML ke papan klip dengan Async Clipboard API, browser akan memastikan bahwa browser tersebut terbentuk dengan baik dengan menjalankannya melalui parser DOM dan membuat serialisasi string HTML yang dihasilkan, tetapi tidak ada sanitasi yang terjadi pada langkah ini. Anda tidak perlu melakukan apa pun. Jika HTML read() ditempatkan pada papan klip oleh aplikasi lain, dan aplikasi web Anda memilih untuk mendapatkan konten fidelitas penuh dan perlu melakukan sanitasi apa pun dalam kode Anda sendiri, Anda dapat meneruskan objek opsi ke metode read() dengan properti unsanitized dan nilai ['text/html']. Secara terpisah, formatnya terlihat seperti ini: navigator.clipboard.read({ unsanitized: ['text/html'] }). Contoh kode berikut di bawah ini hampir sama dengan yang ditampilkan sebelumnya, tetapi kali ini dengan opsi unsanitized. Saat mencobanya di DevTools Console, Anda akan melihat bahwa input dan outputnya 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 ini didukung, jadi deteksi fitur didasarkan pada pengamatan perilaku. Oleh karena itu, contoh berikut bergantung pada deteksi fakta apakah tag <style> bertahan atau tidak, yang menunjukkan dukungan, atau inline, yang menunjukkan non-dukungan. Perhatikan bahwa agar hal ini berfungsi, halaman harus sudah 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 diuraikan dalam pengantar, sebagian besar developer tidak perlu mengkhawatirkan sanitasi papan klip dan dapat menggunakan pilihan sanitasi default yang dibuat oleh browser. Untuk kasus langka saat developer perlu menanganinya, opsi unsanitized tersedia.

Ucapan terima kasih

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