Nicht bereinigter HTML-Code in der Async Clipboard API

Ab Chrome 120 ist in der Async Clipboard API eine neue Option unsanitized verfügbar. Diese Option kann in bestimmten Situationen mit HTML hilfreich sein, in denen Sie den Inhalt der Zwischenablage genau so einfügen müssen, wie er beim Kopieren war. Das heißt, ohne Zwischenschritte zur Bereinigung, die in Browsern üblicherweise und aus gutem Grund angewendet werden. In diesem Leitfaden erfahren Sie, wie Sie die Funktion verwenden.

Bei der Arbeit mit der Async Clipboard API müssen sich Entwickler in den meisten Fällen keine Gedanken über die Integrität der Inhalte in der Zwischenablage machen. Sie können davon ausgehen, dass die Daten, die sie in die Zwischenablage schreiben (kopieren), dieselben sind, die sie erhalten, wenn sie die Daten aus der Zwischenablage lesen (einfügen).

Das gilt definitiv für Text. Fügen Sie den folgenden Code in die DevTools-Konsole ein und lenken Sie den Fokus sofort auf die Seite. (Die setTimeout() ist erforderlich, damit Sie genügend Zeit haben, den Fokus auf die Seite zu legen. Dies ist eine Anforderung der Async Clipboard API.) Wie Sie sehen, ist die Eingabe genau mit der Ausgabe identisch.

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

Bei Bildern sieht das etwas anders aus. Um sogenannte Komprimierungsbomben zu verhindern, codieren Browser Bilder wie PNGs neu. Die Eingabe- und Ausgabebilder sind jedoch visuell pixelgenau identisch.

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

Was passiert aber mit HTML-Text? Wie Sie vielleicht vermutet haben, ist das bei HTML anders. Hier entfernt der Browser <script>-Tags (und andere wie <meta>, <head> und <style>) aus dem HTML-Code und fügt CSS-Code ein, um schädliche Aktionen zu verhindern. Sehen Sie sich das folgende Beispiel an und testen Sie es in der DevTools-Konsole. Sie werden feststellen, dass sich die Ausgabe deutlich von der Eingabe unterscheidet.

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

Die HTML-Bereinigung ist im Allgemeinen eine gute Sache. Sie sollten in den meisten Fällen kein nicht bereinigtes HTML zulassen, da dies Sicherheitsrisiken birgt. Es gibt jedoch Szenarien, in denen der Entwickler genau weiß, was er tut und in denen die Integrität der Eingabe- und Ausgabe-HTML für die ordnungsgemäße Funktion der App entscheidend ist. In diesen Fällen haben Sie zwei Möglichkeiten:

  1. Wenn Sie sowohl das Kopieren als auch das Einfügen steuern, z. B. wenn Sie etwas innerhalb Ihrer App kopieren und dann wieder in Ihrer App einfügen, sollten Sie benutzerdefinierte Webformate für die Async Clipboard API verwenden. Lesen Sie den verlinkten Artikel.
  2. Wenn Sie in Ihrer App nur das Einfügen, aber nicht das Kopieren steuern, z. B. weil der Kopiervorgang in einer nativen App erfolgt, die keine benutzerdefinierten Webformate unterstützt, sollten Sie die Option unsanitized verwenden. Diese wird im Rest dieses Artikels erläutert.

Dazu gehören beispielsweise das Entfernen von script-Tags, das Einfügen von Stilen und das Sicherstellen, dass die HTML-Datei korrekt formatiert ist. Diese Liste ist nicht vollständig und es können in Zukunft weitere Schritte hinzugefügt werden.

Kopieren und Einfügen von nicht bereinigtem HTML

Wenn Sie mit der Async Clipboard API HTML-Code write() (kopieren) in die Zwischenablage einfügen, prüft der Browser, ob er korrekt formatiert ist, indem er ihn durch einen DOM-Parser führt und den resultierenden HTML-String serialisiert. In diesem Schritt wird jedoch keine Bereinigung durchgeführt. Sie müssen nichts weiter tun. Wenn Sie read()-HTML von einer anderen Anwendung in die Zwischenablage kopieren und Ihre Webanwendung die Inhalte in voller Qualität abrufen und in Ihrem eigenen Code bereinigen möchte, können Sie der read()-Methode ein Optionsobjekt mit dem Attribut unsanitized und dem Wert ['text/html'] übergeben. Für sich genommen sieht das so aus: navigator.clipboard.read({ unsanitized: ['text/html'] }). Das folgende Codebeispiel ist fast identisch mit dem vorherigen, enthält aber diesmal die Option unsanitized. Wenn Sie es in der DevTools-Konsole ausprobieren, sehen Sie, dass Eingabe und Ausgabe identisch sind.

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

Browserunterstützung und Funktionserkennung

Es gibt keine direkte Möglichkeit, zu prüfen, ob die Funktion unterstützt wird. Daher basiert die Funktionserkennung auf der Beobachtung des Verhaltens. Daher wird im folgenden Beispiel ermittelt, ob ein <style>-Tag erhalten bleibt, was auf Unterstützung hinweist, oder ob es eingefügt wird, was auf keine Unterstützung hinweist. Damit dies funktioniert, muss die Seite bereits die Berechtigung für die Zwischenablage erhalten haben.

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

Eine Demo der Option unsanitized findest du auf Glitch und den Quellcode dazu.

Ergebnisse

Wie in der Einführung erläutert, müssen sich die meisten Entwickler nie um die Sanitierung des Zwischenspeichers kümmern und können einfach die Standardoptionen für die Sanitierung verwenden, die vom Browser festgelegt werden. Für die seltenen Fälle, in denen Entwickler sich darum kümmern müssen, gibt es die Option unsanitized.

Danksagungen

Dieser Artikel wurde von Anupam Snigdha und Rachel Andrew geprüft. Die API wurde vom Microsoft Edge-Team festgelegt und implementiert.