Code HTML non rectifié dans l'API Async Clipboard

À partir de Chrome 120, une nouvelle option unsanitized est disponible dans l'API Async Clipboard. Cette option peut être utile dans des situations particulières avec le code HTML, lorsque vous devez coller le contenu du presse-papiers tel qu'il était au moment de la copie. C'est-à-dire sans aucune étape de nettoyage intermédiaire que les navigateurs appliquent couramment (et pour de bonnes raisons). Découvrez comment l'utiliser dans ce guide.

Dans la plupart des cas, lorsque vous utilisez l'API Async Clipboard, les développeurs n'ont pas à se soucier de l'intégrité du contenu du presse-papiers et peuvent supposer que ce qu'ils écrivent dans le presse-papiers (copie) est identique à ce qu'ils obtiendront lorsqu'ils liront les données du presse-papiers (collage).

C'est particulièrement vrai pour le texte. Essayez de coller le code suivant dans la console DevTools, puis de recentrer immédiatement la page. (setTimeout() est nécessaire pour que vous ayez suffisamment de temps pour mettre en surbrillance la page, ce qui est une exigence de l'API Async Clipboard.) Comme vous pouvez le constater, l'entrée est exactement identique à la sortie.

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

Avec les images, c'est un peu différent. Pour éviter les attaques appelées bombes de compression, les navigateurs réencodent les images telles que les PNG, mais les images d'entrée et de sortie sont visuellement exactement les mêmes, pixel par pixel.

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

Qu'en est-il du texte HTML ? Comme vous l'avez peut-être deviné, la situation est différente avec le code HTML. Ici, le navigateur nettoie le code HTML pour éviter les problèmes, par exemple en supprimant les balises <script> du code HTML (et d'autres comme <meta>, <head> et <style>) et en insérant du CSS. Prenons l'exemple suivant et essayons-le dans la console DevTools. Vous remarquerez que la sortie diffère considérablement de l'entrée.

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

Le nettoyage du code HTML est généralement une bonne chose. Vous ne voulez pas vous exposer à des problèmes de sécurité en autorisant le code HTML non nettoyé dans la plupart des cas. Il existe toutefois des scénarios où le développeur sait exactement ce qu'il fait et où l'intégrité de l'entrée et de la sortie HTML est cruciale pour le bon fonctionnement de l'application. Dans ces circonstances, vous avez deux choix:

  1. Si vous contrôlez à la fois la fin de la copie et du collage, par exemple, si vous copiez depuis votre application pour ensuite la coller dans votre application, vous devez utiliser des formats Web personnalisés pour l'API Async Clipboard. Arrêtez de lire cet article et consultez celui auquel il est associé.
  2. Si vous ne contrôlez que l'opération de collage dans votre application, mais pas l'opération de copie (par exemple, parce que l'opération de copie se produit dans une application native qui n'est pas compatible avec les formats personnalisés Web), vous devez utiliser l'option unsanitized, qui est expliquée dans la suite de cet article.

Le nettoyage comprend, par exemple, la suppression des balises script, l'intégration de styles et la vérification que le code HTML est bien formaté. Cette liste n'est pas exhaustive, et d'autres étapes pourront être ajoutées à l'avenir.

Copier et coller du code HTML non nettoyé

Lorsque vous write() (copiez) du code HTML dans le presse-papiers avec l'API Async Clipboard, le navigateur s'assure qu'il est bien formé en l'exécutant via un analyseur DOM et en sérialisant la chaîne HTML résultante, mais aucune purification n'est effectuée à cette étape. Vous n'avez rien à faire. Lorsque vous read() du code HTML placé dans le presse-papiers par une autre application, et que votre application Web active l'obtention du contenu haute fidélité et doit effectuer une désinfection dans votre propre code, vous pouvez transmettre un objet d'options à la méthode read() avec une propriété unsanitized et une valeur de ['text/html']. Pris isolément, il se présente comme suit : navigator.clipboard.read({ unsanitized: ['text/html'] }). L'exemple de code suivant est presque identique à celui présenté précédemment, mais cette fois avec l'option unsanitized. Lorsque vous l'essayez dans la console DevTools, vous constatez que l'entrée et la sortie sont identiques.

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

Compatibilité avec les navigateurs et détection des fonctionnalités

Il n'existe aucun moyen direct de vérifier si la fonctionnalité est compatible. La détection de la fonctionnalité est donc basée sur l'observation du comportement. Par conséquent, l'exemple suivant repose sur la détection de la survie d'une balise <style>, ce qui indique la compatibilité, ou de son insertion dans le code, ce qui indique l'incompatibilité. Notez que pour que cela fonctionne, la page doit déjà avoir obtenu l'autorisation de presse-papiers.

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

Démo

Pour voir l'option unsanitized en action, regardez la démonstration sur Glitch et consultez son code source.

Conclusions

Comme indiqué dans l'introduction, la plupart des développeurs n'auront jamais à se soucier de la désinfection du presse-papiers et pourront simplement utiliser les choix de désinfection par défaut effectués par le navigateur. Dans les rares cas où les développeurs doivent s'en soucier, l'option unsanitized existe.

Remerciements

Cet article a été relu par Anupam Snigdha et Rachel Andrew. L'API a été spécifiée et implémentée par l'équipe Microsoft Edge.