From 1127523d16774b960c03a95e92bef7ec25254e55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:45:46 +0000 Subject: [PATCH 1/2] Initial plan From bdbbcc64fe3948c14c168462eabbadc2910f4789 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:51:51 +0000 Subject: [PATCH 2/2] Normalize non-breaking spaces in copied clipboard text --- src/clipboard.ts | 26 +++++++++++++++++++------- test/test.js | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/clipboard.ts b/src/clipboard.ts index 7e14015..365f0b2 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -8,11 +8,17 @@ function createNode(text: string): Element { return node } -export function copyNode(node: Element): Promise { - if ('clipboard' in navigator) { - return navigator.clipboard.writeText(node.textContent || '') - } +// 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 +function normalizeText(text: string): string { + return text.replace(/\u00A0/g, ' ') +} +function copySelection(node: Element): Promise { const selection = getSelection() if (selection == null) { return Promise.reject(new Error()) @@ -29,9 +35,15 @@ export function copyNode(node: Element): Promise { return Promise.resolve() } +export function copyNode(node: Element): Promise { + return copyText(node.textContent || '') +} + export function copyText(text: string): Promise { + const normalized = normalizeText(text) + if ('clipboard' in navigator) { - return navigator.clipboard.writeText(text) + return navigator.clipboard.writeText(normalized) } const body = document.body @@ -39,9 +51,9 @@ export function copyText(text: string): Promise { return Promise.reject(new Error()) } - const node = createNode(text) + const node = createNode(normalized) body.appendChild(node) - copyNode(node) + copySelection(node) body.removeChild(node) return Promise.resolve() } diff --git a/test/test.js b/test/test.js index fa876c1..362e2c1 100644 --- a/test/test.js +++ b/test/test.js @@ -154,6 +154,31 @@ describe('clipboard-copy element', function () { assert.equal(text, 'I am a link') }) + it('normalizes non-breaking spaces to regular spaces', async function () { + const target = document.createElement('div') + target.textContent = 'wget -O - https://example.com/hello.sh\u00A0| bash' + target.id = 'copy-target' + document.body.append(target) + + const button = document.querySelector('clipboard-copy') + button.click() + + const text = await whenCopied + assert.equal(text, 'wget -O - https://example.com/hello.sh | bash') + assert.notInclude(text, '\u00A0') + }) + + it('normalizes non-breaking spaces from the value attribute', async function () { + const button = document.querySelector('clipboard-copy') + button.setAttribute('value', 'hello.sh\u00A0| bash') + + button.click() + + const text = await whenCopied + assert.equal(text, 'hello.sh | bash') + assert.notInclude(text, '\u00A0') + }) + it('does not copy when disabled', async function () { const target = document.createElement('div') target.innerHTML = 'Hello world!'