Async Clipboard API의 정리되지 않은 HTML

Chrome 120부터 Async Clipboard API에서 새로운 unsanitized 옵션을 사용할 수 있습니다. 이 옵션은 클립보드의 콘텐츠를 복사할 때와 동일하게 붙여넣어야 하는 HTML의 특수한 상황에서 유용합니다. 즉, 브라우저에서 합당한 이유로 일반적으로 적용하는 중간 정리 단계가 없습니다. 이 가이드에서 사용 방법을 알아보세요.

Async Clipboard API를 사용할 때 대부분의 경우 개발자는 클립보드에 있는 콘텐츠의 무결성에 관해 걱정할 필요가 없으며 클립보드에 쓰는 내용 (사본)이 클립보드에서 데이터를 읽으면 (붙여넣기) 얻는 것과 동일하다고 가정할 수 있습니다.

이는 텍스트의 경우 확실히 적용됩니다. DevTools 콘솔에 다음 코드를 붙여넣은 다음 즉시 페이지의 포커스를 다시 맞춰 보세요. setTimeout()는 페이지에 포커스를 맞출 충분한 시간을 확보하기 위해 필요하며 이는 Async Clipboard API의 요구사항입니다. 보시다시피 입력은 출력과 정확히 동일합니다.

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

이미지의 경우는 조금 다릅니다. 소위 압축 폭탄 공격을 방지하기 위해 브라우저는 PNG와 같은 이미지를 다시 인코딩하지만 입력 이미지와 출력 이미지는 픽셀 단위로 시각적으로 정확히 동일합니다.

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

HTML 텍스트는 어떻게 되나요? 짐작하셨겠지만 HTML에서는 상황이 다릅니다. 여기서 브라우저는 HTML 코드에서 <script> 태그(및 <meta>, <head>, <style>과 같은 기타 태그)를 삭제하고 CSS를 인라인 처리하는 등의 방식으로 나쁜 일이 발생하지 않도록 HTML 코드를 정리합니다. 다음 예를 살펴보고 DevTools 콘솔에서 사용해 보세요. 출력이 입력과 상당히 다르다는 것을 알 수 있습니다.

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

HTML 정리는 일반적으로 좋은 일입니다. 대부분의 경우 정리되지 않은 HTML을 허용하여 보안 문제에 노출되지 않도록 해야 합니다. 하지만 개발자가 실행 중인 작업을 정확하게 알고 있는 경우와 인앱 및 출력 HTML의 무결성이 앱의 올바른 작동에 필수적인 시나리오가 있습니다. 이러한 상황에서는 다음 두 가지 방법 중 하나를 선택할 수 있습니다.

  1. 복사와 붙여넣기를 모두 제어하는 경우, 예를 들어 앱 내에서 복사하여 앱 내에 붙여넣는 경우 Async Clipboard API의 웹 맞춤 형식을 사용해야 합니다. 여기에서 읽기를 중지하고 링크된 도움말을 확인하세요.
  2. 앱에서 붙여넣기 작업만 제어하고 복사 작업은 제어하지 않는 경우(예: 복사 작업이 웹 맞춤 형식을 지원하지 않는 네이티브 앱에서 실행되기 때문에) 이 도움말의 나머지 부분에서 설명하는 unsanitized 옵션을 사용해야 합니다.

정리에는 script 태그 삭제, 스타일 인라인 처리, HTML 형식 확인 등이 포함됩니다. 이 목록은 일부이며 향후 더 많은 단계가 추가될 수 있습니다.

정리되지 않은 HTML 복사 및 붙여넣기

Async Clipboard API를 사용하여 HTML을 클립보드에 write() (복사)하면 브라우저가 DOM 파서를 통해 실행하고 결과 HTML 문자열을 직렬화하여 올바르게 구성되었는지 확인하지만 이 단계에서는 삭제가 발생하지 않습니다. 별도의 조치는 필요하지 않습니다. 다른 애플리케이션이 클립보드에 배치한 read() HTML을 read()하고 웹 앱이 전체 충실도 콘텐츠를 가져오도록 선택하고 자체 코드에서 제거를 실행해야 하는 경우 unsanitized 속성과 ['text/html'] 값을 사용하여 옵션 객체를 read() 메서드에 전달할 수 있습니다. 격리하면 다음과 같이 표시됩니다. navigator.clipboard.read({ unsanitized: ['text/html'] }) 아래의 다음 코드 샘플은 이전에 보여드린 것과 거의 동일하지만 이번에는 unsanitized 옵션을 사용합니다. DevTools 콘솔에서 시도해 보면 입력과 출력이 동일한 것을 확인할 수 있습니다.

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

브라우저 지원 및 기능 감지

기능이 지원되는지 확인하는 직접적인 방법은 없으므로 기능 감지는 동작을 관찰하는 데 기반합니다. 따라서 다음 예에서는 <style> 태그가 유지되는지(지원됨을 나타냄) 또는 인라인 처리되는지(지원되지 않음을 나타냄)를 감지합니다. 이 기능이 작동하려면 페이지에서 이미 클립보드 권한을 얻어야 합니다.

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

데모

unsanitized 옵션을 사용해 보려면 Glitch의 데모를 보고 소스 코드를 확인하세요.

결론

소개에서 설명한 대로 대부분의 개발자는 클립보드 정리를 걱정할 필요가 없으며 브라우저에서 선택한 기본 정리 옵션을 사용하면 됩니다. 개발자가 신경 써야 하는 드문 경우를 위해 unsanitized 옵션이 있습니다.

감사의 말씀

이 문서는 아누팜 스니그다레이첼 앤드류가 검토했습니다. 이 API는 Microsoft Edge팀에서 지정하고 구현했습니다.