diff --git a/src/clipboard.ts b/src/clipboard.ts index 365f0b2..8e1bb0f 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -8,14 +8,12 @@ function createNode(text: string): Element { return node } -// Some characters, such as the non-breaking space (U+00A0), render -// identically to a regular space but are copied to the clipboard as their -// original, non-printable code point. This can be abused to hide malicious -// content (for example in shell commands) that is invisible in the rendered -// page. Normalizing these characters ensures the copied text matches what the -// user sees. See https://hackerone.com/reports/1805414 +// Normalize characters that render as a space (or as nothing) so that copied +// text matches what the user sees. function normalizeText(text: string): string { - return text.replace(/\u00A0/g, ' ') + return text + .replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, ' ') + .replace(/[\u200B-\u200D\u2060\uFEFF\u180E]/g, '') } function copySelection(node: Element): Promise { diff --git a/test/test.js b/test/test.js index 362e2c1..ab1c7da 100644 --- a/test/test.js +++ b/test/test.js @@ -179,6 +179,61 @@ describe('clipboard-copy element', function () { assert.notInclude(text, '\u00A0') }) + it('normalizes Unicode space-separator characters to regular spaces', async function () { + const spaceChars = [ + '\u00A0', + '\u1680', + '\u2000', + '\u2001', + '\u2002', + '\u2003', + '\u2004', + '\u2005', + '\u2006', + '\u2007', + '\u2008', + '\u2009', + '\u200A', + '\u202F', + '\u205F', + '\u3000', + ] + + for (const char of spaceChars) { + const button = document.querySelector('clipboard-copy') + button.setAttribute('value', `hello.sh${char}| bash`) + + const copied = new Promise(resolve => { + document.addEventListener('clipboard-copy', resolve, {once: true}) + }) + button.click() + await copied + + const text = await navigator.clipboard.readText() + assert.equal(text, 'hello.sh | bash', `failed for U+${char.codePointAt(0).toString(16).toUpperCase()}`) + assert.notInclude(text, char) + } + }) + + it('strips zero-width and invisible format characters', async function () { + const zeroWidthChars = ['\u200B', '\u200C', '\u200D', '\u2060', '\uFEFF', '\u180E'] + + for (const char of zeroWidthChars) { + const button = document.querySelector('clipboard-copy') + button.setAttribute('value', `hello.sh${char}| bash`) + + const copied = new Promise(resolve => { + document.addEventListener('clipboard-copy', resolve, {once: true}) + }) + button.click() + await copied + + const text = await navigator.clipboard.readText() + assert.equal(text, 'hello.sh| bash', `failed for U+${char.codePointAt(0).toString(16).toUpperCase()}`) + assert.notInclude(text, char) + } + }) + it('does not copy when disabled', async function () { const target = document.createElement('div') target.innerHTML = 'Hello world!'