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 HTML, quando devi incollare i contenuti della clipboard in modo identico a come erano al momento della copia. In altre parole, senza alcun passaggio intermedio di sanificazione che i browser applicano comunemente e per validi motivi. Scopri come utilizzarla in questa guida.

Quando si utilizza l'API Clipboard asincrona, nella maggior parte dei casi gli sviluppatori non devono preoccuparsi dell'integrità dei contenuti nella clipboard e possono presumere che ciò che scrivono nella clipboard (copia) sia lo stesso che otterranno quando leggono i dati dalla clipboard (incolla).

Questo è sicuramente vero per il testo. Prova a incollare il seguente codice nella Console DevTools e poi ricentra immediatamente la pagina. Il valore setTimeout() è necessario per avere tempo sufficiente per mettere a fuoco la pagina, un requisito dell'API AsyncClipboard. 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, il discorso è leggermente diverso. Per evitare i cosiddetti attacchi di bombe di compressione, i browser ricodificano le immagini come i file PNG, ma le immagini di input e di output sono visivamente identiche, 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);

Che cosa succede al testo HTML? Come puoi immaginare, con HTML la situazione è diversa. In questo caso, il browser esegue la sanitizzazione del codice HTML per evitare problemi, ad esempio rimuovendo i tag <script> dal codice HTML (e altri come <meta>, <head> e <style>) e inserendo il CSS in linea. Considera l'esempio seguente e provalo nella console DevTools. Noterai che l'output è molto 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);

La sanitizzazione HTML è generalmente una buona cosa. Non vuoi esporrti a problemi di sicurezza consentendo HTML non sanificato nella maggior parte dei casi. Tuttavia, esistono scenari in cui lo sviluppatore sa esattamente cosa sta facendo e in cui l'integrità dell'HTML in entrata e in uscita è fondamentale per il corretto funzionamento dell'app. In queste circostanze, hai due opzioni:

  1. Se controlli sia la copia sia l'incollaggio, ad esempio se copi dall'interno della tua app per poi incollare nello stesso modo all'interno della tua app, devi utilizzare formati personalizzati web per l'API Async Clipboard. Interrompi la lettura qui e consulta l'articolo collegato.
  2. Se nella tua app controlli solo il lato di incollaggio, ma non quello di copia, forse perché l'operazione di copia avviene in un'app nativa che non supporta formati personalizzati web, devi utilizzare l'opzione unsanitized, che è spiegata nel resto di questo articolo.

La sanitizzazione include, ad esempio, la rimozione dei tag script, l'inserimento di stili e la verifica della correttezza del codice HTML. Questo elenco non è esaustivo e in futuro potrebbero essere aggiunti altri passaggi.

Copia e incolla di codice HTML non sanificato

Quando write() (copi) il codice HTML negli appunti con l'API Async Clipboard, il browser si assicura che il formato sia corretto eseguendolo tramite un parser DOM e serializzando la stringa HTML risultante, ma non viene effettuata alcuna sanificazione in questo passaggio. Non devi fare nulla. Quando il codice HTML read() viene inserito nella clipboard da un'altra applicazione e la tua app web attiva l'opzione di acquisizione dei contenuti con fedeltà completa e deve eseguire la sanificazione nel proprio codice, puoi passare un oggetto options al metodo read() con una proprietà unsanitized e un valore ['text/html']. Se usato da solo, ha questo aspetto: navigator.clipboard.read({ unsanitized: ['text/html'] }). L'esempio di codice riportato di seguito è quasi uguale a quello mostrato in precedenza, ma questa volta con l'opzione unsanitized. Quando provi questa operazione nella console DevTools, noterai 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 dei 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. Pertanto, l'esempio seguente si basa sul rilevamento del fatto che un tag <style> sia presente, il che indica il supporto, o sia in linea, il che indica la mancata assistenza. Tieni presente che per il funzionamento di questa operazione, la pagina deve aver già ottenuto l'autorizzazione per accedere alla clipboard.

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 suo codice sorgente.

Conclusioni

Come descritto nell'introduzione, la maggior parte degli sviluppatori non dovrà mai preoccuparsi della sanitizzazione della clipboard e potrà utilizzare semplicemente le scelte di sanitizzazione predefinite effettuate dal browser. Per i rari casi in cui gli sviluppatori devono intervenire, esiste l'opzioneunsanitized.

Ringraziamenti

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