Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions apps/desktop/src/main/browser-agent/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1770,20 +1770,34 @@ describe('browser-agent session', () => {
expect(event.preventDefault).toHaveBeenCalledOnce()
})

it('permission handlers deny every request on the agent partition', () => {
it('permission handlers deny every request on the agent partition but the copy button', () => {
const tab = session.ensureTab()
const ses = (tab.view as unknown as MockView).webContents.session
const requestHandler = ses.setPermissionRequestHandler.mock.calls[0][0] as (
wc: unknown,
permission: string,
callback: (granted: boolean) => void
) => void
const callback = vi.fn()
requestHandler(null, 'media', callback)
expect(callback).toHaveBeenCalledWith(false)
const checkHandler = ses.setPermissionCheckHandler.mock.calls[0][0] as (
wc: unknown,
permission: string
) => boolean

// Reading the clipboard would leak whatever the user last copied anywhere
// else, so it stays denied alongside everything a page could spy through.
for (const permission of ['media', 'geolocation', 'notifications', 'clipboard-read']) {
const callback = vi.fn()
requestHandler(null, permission, callback)
expect(callback).toHaveBeenCalledWith(false)
expect(checkHandler(null, permission)).toBe(false)
}

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

it('leaves nothing of the signed-out user behind in the browser profile', async () => {
Expand Down
27 changes: 23 additions & 4 deletions apps/desktop/src/main/browser-agent/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,16 +810,35 @@ export async function importAgentCookies(
return { imported, failed }
}

/**
* The single site permission a browsing surface cannot withhold: the one every
* "Copy" button on the web goes through. Blanket-denying it made
* `navigator.clipboard.writeText` reject with `NotAllowedError`, so those
* buttons did nothing at all — no error, no copied text — while the legacy
* `document.execCommand('copy')` path kept working, which is why only some
* sites looked broken.
*
* Granting it hands the page no reach it lacked: Chromium still requires the
* document to be focused and to hold a transient user activation, and a
* sanitized write only places text the page already renders onto the clipboard.
* Reading stays denied — that is the direction that would leak whatever the
* user last copied from anywhere else.
*/
const ALLOWED_SITE_PERMISSIONS = new Set(['clipboard-sanitized-write'])

/**
* Default-deny hardening for the agent partition. Site permissions remain
* denied, while uploads use Chromium's native file chooser and downloads are
* saved into the device-level browser download directory.
* denied apart from ALLOWED_SITE_PERMISSIONS, while uploads use Chromium's
* native file chooser and downloads are saved into the device-level browser
* download directory.
*/
function configureAgentPartition(ses: Session): void {
if (configuredPartitions.has(ses)) return
configuredPartitions.add(ses)
ses.setPermissionRequestHandler((_wc, _permission, callback) => callback(false))
ses.setPermissionCheckHandler(() => false)
ses.setPermissionRequestHandler((_wc, permission, callback) =>
callback(ALLOWED_SITE_PERMISSIONS.has(permission))
)
ses.setPermissionCheckHandler((_wc, permission) => ALLOWED_SITE_PERMISSIONS.has(permission))
// Service workers do not inherit a tab's user agent. With only the tab's set,
// the document request carries the browser string while the worker's own
// script request still announces Electron — and on a site that routes its
Expand Down
Loading