From Chrome 120, a new unsanitized
option is available in the Async Clipboard
API. This option can help in special situations with HTML, where you need to
paste the contents of the clipboard identical to how it was when it was copied.
That is, without any intermediate sanitization step that browsers commonly—and
for good reasons—apply. Learn how to use it in this guide.
When working with the Async Clipboard API, in the majority of cases, developers don't need to worry about the integrity of the content on the clipboard and can assume that what they write onto the clipboard (copy) is the same what they will get when they read the data from the clipboard (paste).
This is definitely true for text. Try pasting the following code in the DevTools
Console and then refocus the page immediately. (The setTimeout()
is necessary
so you have enough time to focus the page, which is a requirement of the Async
Clipboard API.) As you see, the input is exactly the same as the 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);
With images, it's a little different. To prevent so-called compression bomb attacks, browsers re-encode images like PNGs, but the input and the output images are visually exactly the same, 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);
What happens with HTML text, though? As you may have guessed, with HTML, the
situation is different. Here, the browser sanitizes the HTML code to prevent bad
things from happening, by, for example, stripping <script>
tags from the HTML
code (and others like <meta>
, <head>
, and <style>
) and by inlining CSS.
Consider the following example and try it in the DevTools Console. You will
notice that the output differs quite significantly from the 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);
HTML sanitization generally is a good thing. You don't want to expose yourself to security issues by allowing unsanitized HTML in the majority of cases. There are scenarios, though, where the developer knows exactly what they are doing and where the integrity of the in- and output HTML is crucial to the correct functioning of the app. Under these circumstances, you have two choices:
- If you control both the copying and the pasting end, for example, if you copy from within your app to then likewise paste within your app, you should use Web custom formats for the Async Clipboard API. Stop reading here and check the linked article.
- If you only control the pasting end in your app, but not the copying end,
maybe because the copy operation happens in a native app that doesn't support
web custom formats, you should use the
unsanitized
option, which is explained in the rest of this article.
Sanitization includes things like removing script
tags, inlining styles, and
ensuring the HTML is well-formed. This list is non-comprehensive, and more
steps may be added in the future.
Copy and paste of unsanitized HTML
When you write()
(copy) HTML to the clipboard with the Async Clipboard API,
the browser makes sure that it is well formed by running it through a DOM parser
and serializing the resulting HTML string, but no sanitization is happening at
this step. There is nothing you need to do. When you read()
HTML placed on the
clipboard by another application, and your web app is opting in to getting the
full fidelity content and needing to perform any sanitization in your own code,
you can pass an options object to the read()
method with a property
unsanitized
and a value of ['text/html']
. In isolation, it looks like this:
navigator.clipboard.read({ unsanitized: ['text/html'] })
. The following code sample
below is almost the same as the one shown previously, but this time with the unsanitized
option. When you try it in the DevTools Console, you will see that the input and
the output are the same.
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);
Browser support and feature detection
There's no direct way to check whether the feature is supported, so feature
detection is based on observing the behavior. Therefore, the following example
relies on the detection of the fact whether a <style>
tag survives, which
indicates support, or is being inlined, which indicates non-support. Note that
for this to work, the page already needs to have obtained the clipboard
permission.
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
To see the unsanitized
option in action, see the
demo on Glitch and check out its
source code.
Conclusions
As outlined in the introduction, most developers will never need to worry about
clipboard sanitization and can just work with the default sanitization choices
made by the browser. For the rare cases where developers need to care, the
unsanitized
option exists.
Useful links
Acknowledgements
This article was reviewed by Anupam Snigdha and Rachel Andrew. The API was specified and implemented by the Microsoft Edge team.