fix(browser-preview): deny runtime permissions in document previews

This commit is contained in:
Jinwoo-H
2026-08-27 18:52:21 -04:00
parent 12fb8e75dc
commit 531b620a44
4 changed files with 62 additions and 13 deletions
@@ -20,6 +20,7 @@ type FakeSession = {
setUserAgent: ReturnType<typeof vi.fn>
setPermissionRequestHandler: ReturnType<typeof vi.fn>
setPermissionCheckHandler: ReturnType<typeof vi.fn>
setDevicePermissionHandler: ReturnType<typeof vi.fn>
setDisplayMediaRequestHandler: ReturnType<typeof vi.fn>
}
@@ -47,6 +48,7 @@ function fakeSession(): FakeSession {
setUserAgent: vi.fn(),
setPermissionRequestHandler: vi.fn(),
setPermissionCheckHandler: vi.fn(),
setDevicePermissionHandler: vi.fn(),
setDisplayMediaRequestHandler: vi.fn()
}
}
@@ -94,7 +96,7 @@ vi.mock('./browser-webauthn-access', () => ({
type PartitionPolicyInstaller = (
profile: BrowserSessionProfile,
options?: { downloads?: 'route' | 'deny' }
options?: { downloads?: 'route' | 'deny'; permissions?: 'browser' | 'deny' }
) => void
// Why imported per test rather than at the top: the installer remembers which partitions it has
@@ -183,3 +185,35 @@ describe('partition download policy', () => {
expect(mocks.handleGuestWillDownload).toHaveBeenCalledTimes(1)
})
})
describe('partition permission policy', () => {
it('denies browser and device permissions on a partition that asks for the deny', async () => {
const install = await loadInstaller()
install(profileFor('orca-doc-preview'), { permissions: 'deny' })
const sess = sessionsByPartition.get('orca-doc-preview')
const requestHandler = sess?.setPermissionRequestHandler.mock.calls[0]?.[0] as (
webContents: { id: number },
permission: string,
callback: (allowed: boolean) => void,
details?: object
) => void
const checkHandler = sess?.setPermissionCheckHandler.mock.calls[0]?.[0] as (
webContents: { id: number },
permission: string,
origin: string,
details?: object
) => boolean
const deviceHandler = sess?.setDevicePermissionHandler.mock.calls[0]?.[0] as (
details: object
) => boolean
for (const permission of ['media', 'clipboard-read', 'notifications']) {
let allowed: boolean | null = null
requestHandler({ id: 42 }, permission, (decision) => (allowed = decision), {})
expect(allowed).toBe(false)
expect(checkHandler({ id: 42 }, permission, 'orca-preview://grant', {})).toBe(false)
}
expect(deviceHandler({})).toBe(false)
})
})
@@ -57,10 +57,16 @@ function resolvePermissionNoticeUrl(
/** `route` hands the item to the owning page's download flow; `deny` cancels it before it starts. */
export type BrowserPartitionDownloadPolicy = 'route' | 'deny'
export type BrowserPartitionPermissionPolicy = 'browser' | 'deny'
type BrowserPartitionPolicyOptions = {
downloads?: BrowserPartitionDownloadPolicy
permissions?: BrowserPartitionPermissionPolicy
}
export function installBrowserSessionPartitionPolicies(
profile: BrowserSessionProfile,
options?: { downloads?: BrowserPartitionDownloadPolicy }
options?: BrowserPartitionPolicyOptions
): void {
const { partition } = profile
const sess = session.fromPartition(partition)
@@ -75,7 +81,12 @@ export function installBrowserSessionPartitionPolicies(
sess.setUserAgent(cleanUA)
setupClientHintsOverride(sess, cleanUA)
}
const denyPermissions = options?.permissions === 'deny'
sess.setPermissionRequestHandler((webContents, permission, callback, details) => {
if (denyPermissions) {
callback(false)
return
}
// Why: defer media to macOS TCC; denying at the session layer throws NotAllowedError even after the user granted Camera/Mic to the OS.
if (permission === 'media') {
// Capture before async handling; opaque frames cannot be attributed to a named site.
@@ -117,6 +128,9 @@ export function installBrowserSessionPartitionPolicies(
callback(allowed)
})
sess.setPermissionCheckHandler((_webContents, permission, _origin, details) => {
if (denyPermissions) {
return false
}
if (permission === 'media') {
return hasSystemMediaAccess(details?.mediaType)
}
@@ -125,7 +139,11 @@ export function installBrowserSessionPartitionPolicies(
}
return isAutoGrantedBrowserSessionPermission(permission)
})
installBrowserWebAuthnAccessHandlers(sess)
if (denyPermissions) {
sess.setDevicePermissionHandler(() => false)
} else {
installBrowserWebAuthnAccessHandlers(sess)
}
sess.setDisplayMediaRequestHandler((_request, callback) => {
callback({ video: undefined, audio: undefined })
})
@@ -261,8 +261,8 @@ describe('installDocPreviewProtocolHandler', () => {
expect(cancelled(`orca-preview://${'a'.repeat(32)}/index.html`)).toBe(false)
})
// Why: preview guests are webviews like any other, so they must not skip the deny-by-default
// permission and display-media policy every browser partition gets.
// Why: preview guests need the shared session policy installer even though they deny more than
// user-opened browser pages.
it('applies the shared browser partition policies to the preview session', () => {
installDocPreviewProtocolHandler()
@@ -272,14 +272,12 @@ describe('installDocPreviewProtocolHandler', () => {
)
})
// Why downloads and nothing else: the browser download flow attributes a file to the page that
// asked for it, and a previewed document is no page. Routed, it would write remote-authored bytes
// into this desktop's Downloads folder with no prompt and no tab to name as the source.
it('asks for downloads to be denied on the preview partition', () => {
it('asks for downloads and runtime permissions to be denied on the preview partition', () => {
installDocPreviewProtocolHandler()
expect(mocks.installBrowserSessionPartitionPolicies).toHaveBeenCalledWith(expect.anything(), {
downloads: 'deny'
downloads: 'deny',
permissions: 'deny'
})
})
})
+2 -3
View File
@@ -124,8 +124,7 @@ export function installDocPreviewProtocolHandler(): void {
previewSession.webRequest.onBeforeRequest((details, callback) => {
callback({ cancel: !isAllowedDocPreviewRequestUrl(details.url) })
})
// Why: preview guests are webviews like any other, so they inherit the same deny-by-default
// permission, display-media and user-agent policy every browser partition gets.
// Why: agent-authored previews must not inherit the grants needed by user-opened browser pages.
installBrowserSessionPartitionPolicies(
{
id: DOC_PREVIEW_PARTITION,
@@ -139,6 +138,6 @@ export function installDocPreviewProtocolHandler(): void {
// page to attribute the file to, and a previewed document is not one. Routed here it would
// reserve a name in this desktop's Downloads folder and write remote-authored bytes into it
// with nothing in the UI naming the tab that asked, and no prompt in front of it.
{ downloads: 'deny' }
{ downloads: 'deny', permissions: 'deny' }
)
}