mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix(browser-preview): deny runtime permissions in document previews
This commit is contained in:
@@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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' }
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user