Nieoczyszczony kod HTML w interfejsie Async Clipboard API

Od wersji 120 Chrome nowa opcja unsanitized jest dostępna w interfejsie API asynchronicznej funkcji Schowek. Ta opcja może być przydatna w szczególnych sytuacjach związanych z HTML, gdy musisz wkleić zawartość schowka w takim samym formacie, w jakim została skopiowana. Oznacza to, że nie ma żadnego pośredniego etapu sterylizacji, którego przeglądarki zwykle używają (i z dobrych powodów). W tym przewodniku dowiesz się, jak z niego korzystać.

Podczas pracy z niesynchronizowanym interfejsem API Schowka w większości przypadków programiści nie muszą się martwić o integralność treści na schowku i mogą założyć, że to, co napiszą na schowku (kopia), jest takie samo, co otrzymają, gdy odczytują dane ze schowka (wklejanie).

Zdecydowanie dotyczy to tekstu. Spróbuj wkleić ten kod w Konsoli DevTools, a następnie natychmiast przekieruj fokus na stronę. (Element setTimeout() jest potrzebny, aby dać użytkownikowi wystarczająco dużo czasu na skupienie się na stronie, co jest wymagane przez interfejs Clipboard API asynchronicznego). Jak widać, dane wejściowe są identyczne z danymi wyjściowymi.

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

W przypadku obrazów jest to nieco inne. Aby zapobiec tak zwanym atakom bomby kompresji, przeglądarki ponownie kodują obrazy w formacie PNG, ale obrazy wejściowe i wyjściowe są wizualnie dokładnie takie same, piksel po pikselu.

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 się zapewne domyślasz, w przypadku kodu HTML sytuacja wygląda inaczej. W tym przypadku przeglądarka czyści kod HTML, aby zapobiec niepożądanym działaniom, na przykład usuwając z kodu HTML tagi <script> (oraz inne, takie jak <meta>, <head><style>) oraz wstawiając kod CSS. Zapoznaj się z tym przykładem i spróbuj go w konsoli DevTools. Zauważysz, że wynik różni się dość znacznie 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 zwykle korzystna. Większość problemów z bezpieczeństwem wynika z dostępu do nieoczyszczonego kodu HTML. Są jednak sytuacje, w których deweloper wie dokładnie, co robi, a całość i struktura kodu HTML na wejściu i na wyjściu są kluczowe dla prawidłowego działania aplikacji. W takich okolicznościach masz 2 możliwości:

  1. Jeśli kontrolujesz zarówno kopiowanie, jak i wklejanie, np. kopiujesz z aplikacji, a potem wklejasz w tej samej aplikacji, użyj niestandardowych formatów internetowych w interfejsie Async Clipboard API. Nie musisz czytać dalej. Przejdź do powiązanego artykułu.
  2. Jeśli w swojej aplikacji kontrolujesz tylko wklejanie, a nie kopiowanie, być może dlatego, że operacja kopiowania odbywa się w natywnej aplikacji, która nie obsługuje formatów internetowych, użyj opcji unsanitized, której działanie jest opisane w dalszej części tego artykułu.

Sanityzacja obejmuje m.in. usuwanie tagów script, wstawianie stylów wbudowanych oraz sprawdzanie, czy kod HTML jest poprawny. Ta lista nie jest wyczerpująca. W przyszłości możemy dodać do niej kolejne kroki.

Kopiowanie i wklejanie nieoczyszczonego kodu HTML

Gdy write() (kopiujesz) kod HTML do schowka za pomocą interfejsu Async Clipboard API, przeglądarka sprawdza, czy jest on poprawnie sformatowany, przesyłając go do parsowania DOM i serializując wynikowy ciąg znaków HTML. W tym kroku nie jest jednak przeprowadzana żadna sanityzacja. Nie musisz nic robić. Gdy read() HTML umieszczony w schowku przez inną aplikację, a Twoja aplikacja internetowa chce uzyskać pełną jakość treści i wykonać dezynfekcję w swoim kodzie, możesz przekazać obiekt opcji do metody read() z własnością unsanitized i wartością ['text/html']. Samodzielnie wygląda tak:navigator.clipboard.read({ unsanitized: ['text/html'] }) Poniższy przykład kodu jest prawie taki sam jak poprzedni, ale tym razem z opcją unsanitized. Gdy spróbujesz tego w konsoli DevTools, 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 ma bezpośredniego sposobu sprawdzenia, czy dana funkcja jest obsługiwana, więc wykrywanie funkcji opiera się na obserwacji zachowania. Dlatego w tym przykładzie wykrywanie polega na sprawdzaniu, czy tag <style> przetrwał, co oznacza obsługę, czy został wstawiony, co oznacza brak obsługi. Pamiętaj, że aby to działało, strona musi mieć już udzielone 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);
};

Prezentacja

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

Podsumowanie

Jak wspomniano we wstępie, większość deweloperów nie musi się martwić o czyszczenie schowka i może korzystać z domyślnych opcji czyszczonych przez przeglądarkę. W rzadkich przypadkach, gdy programiści muszą się czymś zająć, dostępna jest opcja unsanitized.

Podziękowania

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