Nieoczyszczony kod HTML w interfejsie Async Clipboard API

Od Chrome 120 w interfejsie Async Clipboard API dostępna jest nowa opcja unsanitized. Ta opcja może być pomocna w specjalnych sytuacjach, gdy w kodzie HTML trzeba wkleić zawartość schowka i tak samo jak podczas kopiowania. Oznacza to, że nie wymaga to przeprowadzania pośredniej procedury oczyszczania, którą zwykle stosuje się w przeglądarkach – i to z uzasadnionych powodów. Z tego przewodnika dowiesz się, jak z niego korzystać.

Podczas pracy z interfejsem Async Clipboard API w większości przypadków deweloperzy nie muszą martwić się o integralność treści w schowku. Mogą oni założyć, że zapisując w schowku (kopia) dane są takie same, jakie uzyskają po odczytaniu danych ze schowka (wklejenia).

To samo dotyczy tekstu. Wklej ten kod w konsoli DevTools, a następnie od razu zmień tematykę strony. (Parametr setTimeout() jest niezbędny, aby zapewnić wystarczająco dużo czasu na skupienie się na stronie, co jest wymagane w przypadku interfejsu AsyncClipboard API). Jak widać, dane wejściowe są dokładnie takie same jak dane wyjściowe.

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

Jeśli chodzi o obrazy, wygląda to trochę inaczej. Aby zapobiec tzw. bombom kompresyjnym, przeglądarki ponownie kodują obrazy (np. pliki PNG), ale obrazy wejściowe i wyjściowe są wizualne dokładnie takie same, czyli w pikselach na 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);

Co się jednak dzieje z tekstem HTML? Jak można się domyślić, w przypadku HTML sytuacja jest inna. Aby zapobiec niepożądanym zjawiskom, przeglądarka czyści wtedy kod HTML, na przykład usuwając tagi <script> z kodu HTML (i innych, takich jak <meta>, <head> i <style>) i wbudowając CSS. Przeanalizuj poniższy przykład i wypróbuj go w konsoli Narzędzi deweloperskich. Widzimy, że dane wyjściowe znacznie różnią się od danych wejściowych.

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

Sanityzacja kodu HTML jest ogólnie dobrym rozwiązaniem. W większości przypadków nie warto narażać się na problemy z bezpieczeństwem, zezwalając na niesprawdzony kod HTML. Istnieją jednak sytuacje, w których deweloper dokładnie wie, co robi i gdzie integralność wbudowanego i wyjściowego kodu HTML ma kluczowe znaczenie dla prawidłowego działania aplikacji. W takich sytuacjach masz do wyboru 2 możliwości:

  1. Jeśli masz kontrolę zarówno nad kopiowaniem, jak i wklejaniem treści, np. gdy kopiujesz coś z aplikacji, a następnie wklejasz ją w podobny sposób, użyj niestandardowych formatów internetowych dla interfejsu Async Clipboard API. Czytaj dalej i zapoznaj się z podlinkowanym artykułem.
  2. Jeśli kontrolujesz tylko koniec wklejania w aplikacji, a nie koniec kopiowania, być może operacja kopiowania odbywa się w aplikacji natywnej, która nie obsługuje niestandardowych formatów internetowych, użyj opcji unsanitized, która została omówiona w dalszej części tego artykułu.

Sanityzacja obejmuje takie działania jak usunięcie tagów script, wbudowanie stylów oraz sprawdzenie poprawności składni kodu HTML. Ta lista nie jest wyczerpująca, a w przyszłości możemy dodać więcej czynności.

Kopiowanie i wklejanie niesprawdzonego kodu HTML

Gdy write() (kopiujesz) kod HTML do schowka za pomocą interfejsu Async Clipboard API, przeglądarka sprawdza jego format, uruchamiając go za pomocą parsera DOM i zserializując wynikowy ciąg HTML, ale na tym etapie nie odbywa się proces oczyszczania. Nie musisz nic robić. Gdy umieszczasz kod HTML read() w schowku przez inną aplikację, a Twoja aplikacja internetowa akceptuje pobieranie pełnej wierności treści i musi przeprowadzić proces oczyszczania w swoim kodzie, możesz przekazać do metody read() obiekt opcji z właściwością unsanitized i wartością ['text/html']. W przeciwnym razie wygląda to tak: navigator.clipboard.read({ unsanitized: ['text/html'] }). Poniższy przykładowy kod jest prawie taki sam jak kod pokazanego wcześniej, ale tym razem zawiera opcję unsanitized. Po wypróbowaniu go w konsoli Narzędzi deweloperskich zobaczysz, że dane wejściowe i wyjściowe są takie same.

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

Obsługa przeglądarek i wykrywanie funkcji

Nie można bezpośrednio sprawdzić, czy dana funkcja jest obsługiwana, więc wykrywanie cech odbywa się na podstawie obserwacji zachowania. Poniższy przykład dotyczy więc wykrycia tego, czy tag <style> nadal działa, co wskazuje na wsparcie lub jest wbudowane, co oznacza, że tag nie jest obsługiwany. Pamiętaj, że aby to rozwiązanie zadziałało, strona musi już mieć uprawnienia do schowka.

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

Wersja demonstracyjna

Aby zobaczyć, jak działa opcja unsanitized, obejrzyj prezentację dotyczącą Glitch i zapoznaj się z jej kodem źródłowym.

Podsumowanie

Jak wskazano we wstępie, większość deweloperów nie będzie musiała się martwić o uczyszczenie schowka. Użytkownicy mogą po prostu korzystać z domyślnych opcji czyszczenia skonfigurowanych przez przeglądarkę. W rzadkich przypadkach, gdy deweloperzy muszą się tym zająć, możesz użyć opcji unsanitized.

Podziękowania

Ten artykuł został opublikowany przez Anupam Snigdha i Rachel Andrew. Interfejs API został określony i wdrożony przez zespół Microsoft Edge.