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 コードをサニタイズして、HTML コードから <script>
タグ(および <meta>
、<head>
、<style>
など)を削除したり、CSS をインライン化したりすることで、不正な動作を防ぎます。次の例を参考に、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 の入出力の完全性が不可欠なシナリオもあります。このような状況では、次の 2 つの方法があります。
- コピーと貼り付けの両方を制御する場合(たとえば、アプリ内からコピーしてアプリ内に貼り付ける場合など)、Async Clipboard API 用のウェブ カスタム フォーマットを使用する必要があります。ここまでお読みになったら、リンク先の記事をご確認ください。
- アプリで貼り付け側のみを制御し、コピー側は制御しない場合は、
unsanitized
オプションを使用する必要があります。これは、コピー オペレーションがウェブ カスタム形式をサポートしていないネイティブ アプリで行われるためです。このオプションについては、この記事の後半で説明します。
サニタイズには、script
タグの削除、スタイルのインライン化、HTML の整形式化などが含まれます。このリストは包括的なものではなく、今後さらに多くの手順が追加される可能性があります。
未処理の HTML のコピーと貼り付け
Async Clipboard API を使用して HTML をクリップボードに write()
(コピー)すると、ブラウザは DOM パーサーを実行して結果の HTML 文字列をシリアル化し、HTML が適切に形成されていることを確認します。ただし、このステップではサニタイズは行われません。何もする必要はありません。別のアプリによってクリップボードに配置された 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
オプションがあります。
役に立つリンク
謝辞
この記事は、Anupam Snigdha と Rachel Andrew によってレビューされました。この API は Microsoft Edge チームによって指定、実装されました。