HTML non sottoposto a sanitizzazione nell'API Async Clipboard

A partire da Chrome 120, nell'API Async Clipboard è disponibile una nuova opzione unsanitized. Questa opzione può essere utile in situazioni speciali con codice HTML, in cui è necessario incollare i contenuti degli appunti con lo stesso identico aspetto di quando sono stati copiati. Ciò significa che senza alcuna fase di sanitizzazione intermedia che i browser comunemente, e per buoni motivi, applicano. Scopri come usarla in questa guida.

Quando utilizzano l'API Async Clipboard, nella maggior parte dei casi gli sviluppatori non devono preoccuparsi dell'integrità dei contenuti negli appunti e possono presumere che ciò che scrivono negli appunti (copia) sia lo stesso a cui otterranno leggendo i dati dagli appunti (incolla).

Questo vale decisamente per il testo. Prova a incollare il codice che segue nella console di DevTools e imposta di nuovo lo stato attivo della pagina immediatamente. (setTimeout() è necessario per avere tempo sufficiente per impostare lo stato attivo sulla pagina, requisito dell'API Async Clipboard). Come puoi vedere, l'input è esattamente uguale all'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);

Con le immagini, è un po' diverso. Per evitare i cosiddetti attacchi compression bomb, i browser ricodificano immagini come i PNG, ma le immagini di input e quelle di output sono visivamente esattamente le stesse, pixel per 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);

Cosa accade con il testo HTML? Come avrai intuito, nel formato HTML la situazione è diversa. In questo caso, il browser pulisce il codice HTML per evitare che ciò accada, ad esempio rimuovendo i tag <script> dal codice HTML (e altri come <meta>, <head> e <style>) e incorporando CSS. Considera l'esempio seguente e provalo nella console DevTools. Noterai che l'output è piuttosto diverso dall'input.

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

Generalmente, la sanitizzazione del codice HTML è consigliata. Per evitare problemi di sicurezza, è preferibile non consentire la presenza di codice HTML non verificato nella maggior parte dei casi. Esistono però casi in cui lo sviluppatore sa esattamente cosa sta facendo e in cui l'integrità del codice HTML di input e di output è fondamentale per il corretto funzionamento dell'app. In questi casi, hai due opzioni:

  1. Se controlli sia l'estremità da copiare che quella da incollare, ad esempio se copi dalla tua app e poi incolli all'interno dell'app, devi usare Formati personalizzati web per l'API Async Clipboard. Interrompi la lettura qui e controlla l'articolo collegato.
  2. Se controlli solo l'estremità incollata nell'app, ma non l'estremità della copia, magari perché l'operazione di copia avviene in un'app nativa che non supporta i formati personalizzati per il web, ti consigliamo di utilizzare l'opzione unsanitized, descritta nel resto di questo articolo.

La sanitizzazione include, ad esempio, la rimozione di tag script, l'incorporamento degli stili e la garanzia che il codice HTML abbia un formato corretto. Questo elenco non è completo e in futuro potrebbero essere aggiunti altri passaggi.

Copia e incolla di HTML non verificato

Quando write() (copi) l'HTML negli appunti con l'API Async Clipboard, il browser verifica che il formato sia corretto eseguendolo tramite un parser DOM e serializzando la stringa HTML risultante, ma in questo passaggio non viene eseguita alcuna sanitizzazione. Non devi fare nulla. Quando read() hai inserito l'HTML negli appunti da un'altra applicazione e la tua app web sta attivando la ricezione di tutti i contenuti fedeltà e deve eseguire una sanitizzazione nel tuo codice, puoi passare un oggetto opzioni al metodo read() con una proprietà unsanitized e un valore ['text/html']. In isolamento, ha il seguente aspetto: navigator.clipboard.read({ unsanitized: ['text/html'] }). Il seguente esempio di codice riportato di seguito è quasi uguale a quello mostrato in precedenza, ma questa volta con l'opzione unsanitized. Quando lo provi nella console DevTools, vedrai che l'input e l'output sono gli stessi.

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

Supporto del browser e rilevamento delle funzionalità

Non esiste un modo diretto per verificare se la funzionalità è supportata, pertanto il rilevamento della funzionalità si basa sull'osservazione del comportamento. Di conseguenza, l'esempio seguente si basa sul rilevamento del fatto che un tag <style> sia sopravvissuto, il che indica il supporto, oppure è in linea, il che indica il mancato supporto. Tieni presente che, affinché questa operazione funzioni, è necessario che la pagina abbia già ottenuto l'autorizzazione per gli appunti.

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

Per vedere l'opzione unsanitized in azione, guarda la demo su Glitch e controlla il relativo codice sorgente.

Conclusioni

Come descritto nell'introduzione, la maggior parte degli sviluppatori non dovrà mai preoccuparsi della sanitizzazione degli appunti e potrà semplicemente lavorare con le scelte di sanitizzazione predefinite effettuate dal browser. Per i rari casi in cui gli sviluppatori devono preoccuparsene, è disponibile l'opzione unsanitized.

Ringraziamenti

Questo articolo è stato esaminato da Anupam Snigdha e Rachel Andrew. L'API è stata specificata e implementata dal team Microsoft Edge.