Skip to content

Commit 0650eab

Browse files
authored
fix(desktop): let sites copy to the clipboard in the browser tab (#6696)
The agent partition denied every site permission, which included clipboard-sanitized-write — the permission Chromium routes navigator.clipboard.writeText through. Copy buttons rejected with NotAllowedError and did nothing at all: no error, no copied text. Sites still on document.execCommand('copy') kept working, which is why only some looked broken. Granting it hands the page no reach it lacked. Chromium still requires the document to be focused and holding a transient user activation, and a sanitized write only places text the page already renders onto the clipboard. Reading stays denied, along with media, geolocation, and notifications.
1 parent 0239db8 commit 0650eab

2 files changed

Lines changed: 43 additions & 10 deletions

File tree

apps/desktop/src/main/browser-agent/session.test.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,20 +1770,34 @@ describe('browser-agent session', () => {
17701770
expect(event.preventDefault).toHaveBeenCalledOnce()
17711771
})
17721772

1773-
it('permission handlers deny every request on the agent partition', () => {
1773+
it('permission handlers deny every request on the agent partition but the copy button', () => {
17741774
const tab = session.ensureTab()
17751775
const ses = (tab.view as unknown as MockView).webContents.session
17761776
const requestHandler = ses.setPermissionRequestHandler.mock.calls[0][0] as (
17771777
wc: unknown,
17781778
permission: string,
17791779
callback: (granted: boolean) => void
17801780
) => void
1781-
const callback = vi.fn()
1782-
requestHandler(null, 'media', callback)
1783-
expect(callback).toHaveBeenCalledWith(false)
1781+
const checkHandler = ses.setPermissionCheckHandler.mock.calls[0][0] as (
1782+
wc: unknown,
1783+
permission: string
1784+
) => boolean
1785+
1786+
// Reading the clipboard would leak whatever the user last copied anywhere
1787+
// else, so it stays denied alongside everything a page could spy through.
1788+
for (const permission of ['media', 'geolocation', 'notifications', 'clipboard-read']) {
1789+
const callback = vi.fn()
1790+
requestHandler(null, permission, callback)
1791+
expect(callback).toHaveBeenCalledWith(false)
1792+
expect(checkHandler(null, permission)).toBe(false)
1793+
}
17841794

1785-
const checkHandler = ses.setPermissionCheckHandler.mock.calls[0][0] as () => boolean
1786-
expect(checkHandler()).toBe(false)
1795+
// Chromium routes navigator.clipboard.writeText through this one; denying
1796+
// it silently broke every copy button that does not use execCommand.
1797+
const writeCallback = vi.fn()
1798+
requestHandler(null, 'clipboard-sanitized-write', writeCallback)
1799+
expect(writeCallback).toHaveBeenCalledWith(true)
1800+
expect(checkHandler(null, 'clipboard-sanitized-write')).toBe(true)
17871801
})
17881802

17891803
it('leaves nothing of the signed-out user behind in the browser profile', async () => {

apps/desktop/src/main/browser-agent/session.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -810,16 +810,35 @@ export async function importAgentCookies(
810810
return { imported, failed }
811811
}
812812

813+
/**
814+
* The single site permission a browsing surface cannot withhold: the one every
815+
* "Copy" button on the web goes through. Blanket-denying it made
816+
* `navigator.clipboard.writeText` reject with `NotAllowedError`, so those
817+
* buttons did nothing at all — no error, no copied text — while the legacy
818+
* `document.execCommand('copy')` path kept working, which is why only some
819+
* sites looked broken.
820+
*
821+
* Granting it hands the page no reach it lacked: Chromium still requires the
822+
* document to be focused and to hold a transient user activation, and a
823+
* sanitized write only places text the page already renders onto the clipboard.
824+
* Reading stays denied — that is the direction that would leak whatever the
825+
* user last copied from anywhere else.
826+
*/
827+
const ALLOWED_SITE_PERMISSIONS = new Set(['clipboard-sanitized-write'])
828+
813829
/**
814830
* Default-deny hardening for the agent partition. Site permissions remain
815-
* denied, while uploads use Chromium's native file chooser and downloads are
816-
* saved into the device-level browser download directory.
831+
* denied apart from ALLOWED_SITE_PERMISSIONS, while uploads use Chromium's
832+
* native file chooser and downloads are saved into the device-level browser
833+
* download directory.
817834
*/
818835
function configureAgentPartition(ses: Session): void {
819836
if (configuredPartitions.has(ses)) return
820837
configuredPartitions.add(ses)
821-
ses.setPermissionRequestHandler((_wc, _permission, callback) => callback(false))
822-
ses.setPermissionCheckHandler(() => false)
838+
ses.setPermissionRequestHandler((_wc, permission, callback) =>
839+
callback(ALLOWED_SITE_PERMISSIONS.has(permission))
840+
)
841+
ses.setPermissionCheckHandler((_wc, permission) => ALLOWED_SITE_PERMISSIONS.has(permission))
823842
// Service workers do not inherit a tab's user agent. With only the tab's set,
824843
// the document request carries the browser string while the worker's own
825844
// script request still announces Electron — and on a site that routes its

0 commit comments

Comments
 (0)