HTML bermasalah di Async Clipboard API

Mulai Chrome 120, opsi unsanitized baru tersedia di Papan Klip Asinkron Compute Engine API. Opsi ini dapat membantu dalam situasi khusus dengan HTML, di mana Anda harus menempel konten {i>clipboard<i} sesuai dengan ketika konten disalin. Artinya, tanpa langkah sanitasi perantara yang biasanya oleh browser—dan untuk alasan yang baik—ajukan permohonan. Pelajari cara menggunakannya dalam panduan ini.

Saat bekerja dengan API Papan Klip Asinkron, dalam kebanyakan kasus, pengembang tidak perlu mengkhawatirkan integritas konten di papan klip dan dapat mengasumsikan bahwa apa yang mereka tulis ke papan klip (copy) adalah penyimpanan yang sama dengan yang akan mereka dapatkan ketika membaca data dari {i>clipboard<i} (tempel).

Hal ini memang benar untuk teks. Coba tempelkan kode berikut di DevTools Konsol, lalu segera fokuskan ulang halaman. (setTimeout() diperlukan sehingga Anda punya cukup waktu untuk memfokuskan halaman, yang merupakan persyaratan API Papan Klip.) Seperti yang Anda lihat, inputnya 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, akan sedikit berbeda. Untuk mencegah apa yang disebut serangan bom kompresi, browser mengenkode ulang gambar seperti PNG, tetapi gambar input dan outputnya secara visual sama persis, yaitu 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, situasi yang berbeda. Di sini, browser membersihkan kode HTML untuk mencegah hal agar tidak terjadi, misalnya dengan menghapus tag <script> dari HTML (dan lainnya seperti <meta>, <head>, dan <style>) serta dengan menyisipkan CSS. Pertimbangkan contoh berikut dan cobalah di Konsol DevTools. Anda akan perhatikan bahwa {i>output<i} 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);

Sanitasi HTML umumnya adalah hal yang baik. Anda tidak ingin mengekspos diri terhadap masalah keamanan dengan membolehkan HTML yang bermasalah dalam banyak kasus. Ada adalah skenario, di mana pengembang tahu tepat apa yang mereka lakukan dan di mana integritas HTML dalam-dan-{i>output <i}sangat penting untuk fungsi aplikasi. Dalam situasi ini, Anda memiliki dua pilihan:

  1. Jika Anda mengontrol proses penyalinan dan penempelan, misalnya, jika Anda menyalin dari dalam aplikasi kemudian menempel di 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 akhiran penempelan di aplikasi Anda, tetapi tidak ujung penyalinan, mungkin karena operasi penyalinan terjadi di aplikasi asli 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 bahwa HTML tersusun dengan baik. Daftar ini tidak komprehensif, dan banyak lagi langkah-langkah tambahan mungkin akan ditambahkan di masa mendatang.

Salin dan tempel HTML yang bermasalah

Saat Anda write() (menyalin) HTML ke papan klip dengan API Papan Klip Asinkron, browser memastikan bahwa tersusun dengan baik dengan menjalankannya melalui parser DOM dan membuat serialisasi string HTML yang dihasilkan, tetapi tidak ada sanitasi yang terjadi di langkah ini. Anda tidak perlu melakukan apa pun. Saat Anda read() HTML ditempatkan pada papan klip oleh aplikasi lain, dan aplikasi web Anda ikut serta untuk mendapatkan konten dengan fidelitas lengkap dan perlu melakukan sanitasi 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 ini hampir sama dengan yang ditampilkan sebelumnya, tetapi kali ini dengan unsanitized sebelumnya. Saat Anda 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 tersebut didukung, jadi deteksi didasarkan pada pengamatan perilaku. Oleh karena itu, contoh berikut bergantung pada deteksi fakta apakah tag <style> bertahan, yang menunjukkan dukungan, atau sedang disisipkan, yang menunjukkan non-dukungan. Perlu diketahui bahwa agar berfungsi, halaman harus memiliki {i>clipboard<i} izin akses.

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 sumber.

Kesimpulan

Seperti yang diuraikan dalam pendahuluan, sebagian besar pengembang tidak perlu khawatir tentang sanitasi papan klip dan hanya dapat berfungsi dengan opsi sanitasi default yang dibuat oleh browser. Untuk kasus yang jarang terjadi ketika developer perlu peduli, Opsi unsanitized ada.

Ucapan terima kasih

Artikel ini diulas oleh Anupam Snigdha dan Rachel Andrew. API ditentukan dan yang diterapkan oleh tim Microsoft Edge.