Skip to content

Commit 385a1ae

Browse files
hi-ogawacodex
andauthored
fix(browser): disable client cdp API when allowWrite/allowExec: false [backport to v3] (#10456)
Co-authored-by: Codex <noreply@openai.com>
1 parent af88b1f commit 385a1ae

13 files changed

Lines changed: 143 additions & 24 deletions

File tree

docs/guide/browser/commands.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ expect(input).toHaveValue('a')
5959

6060
::: warning
6161
CDP session works only with `playwright` provider and only when using `chromium` browser. You can read more about it in playwright's [`CDPSession`](https://playwright.dev/docs/api/class-cdpsession) documentation.
62+
63+
CDP is a privileged debugging API. It is available only when browser API write and exec operations are enabled through [`browser.api.allowWrite`](/guide/browser/config#browser-api-allowwrite), [`browser.api.allowExec`](/guide/browser/config#browser-api-allowexec), [`api.allowWrite`](/config/#api-allowwrite), and [`api.allowExec`](/config/#api-allowexec).
6264
:::
6365

6466
## Custom Commands

docs/guide/browser/config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ Configure options for Vite server that serves code in the browser. Does not affe
155155
- **Type:** `boolean`
156156
- **Default:** inherited from [`api.allowWrite`](/config/#api-allowwrite)
157157

158-
Allows browser API clients to write files, including snapshots and browser command writes. If `browser.api.host` is set to anything other than `localhost` or `127.0.0.1`, Vitest disables write operations by default unless this option or [`api.allowWrite`](/config/#api-allowwrite) is explicitly enabled.
158+
Allows browser API clients to write files, including snapshots and browser command writes. If `browser.api.host` is set to anything other than `localhost` or `127.0.0.1`, Vitest disables write operations by default unless this option or [`api.allowWrite`](/config/#api-allowwrite) is explicitly enabled. This option also gates privileged browser APIs that can write files indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/guide/browser/context#cdp).
159159

160160
### browser.api.allowExec {#browser-api-allowexec}
161161

162162
- **Type:** `boolean`
163163
- **Default:** inherited from [`api.allowExec`](/config/#api-allowexec)
164164

165-
Allows browser API clients to run tests from the UI. If `browser.api.host` is exposed to the network and write/exec operations are enabled, anyone who can reach the browser API server can run arbitrary code on your machine.
165+
Allows browser API clients to run tests from the UI. If `browser.api.host` is exposed to the network and write/exec operations are enabled, anyone who can reach the browser API server can run arbitrary code on your machine. This option also gates privileged browser APIs that can execute code indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/guide/browser/context#cdp).
166166

167167
## browser.provider {#browser-provider}
168168

docs/guide/browser/context.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ The `cdp` export returns the current Chrome DevTools Protocol session. It is mos
116116
117117
::: warning
118118
CDP session works only with `playwright` provider and only when using `chromium` browser. You can read more about it in playwright's [`CDPSession`](https://playwright.dev/docs/api/class-cdpsession) documentation.
119+
120+
CDP is a privileged debugging API. It is available only when browser API write and exec operations are enabled through [`browser.api.allowWrite`](/guide/browser/config#browser-api-allowwrite), [`browser.api.allowExec`](/guide/browser/config#browser-api-allowexec), [`api.allowWrite`](/config/#api-allowwrite), and [`api.allowExec`](/config/#api-allowexec).
119121
:::
120122
121123
```ts
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { BrowserCommand } from 'vitest/node'
2+
import type { BrowserServerCDPHandler } from '../cdp'
3+
4+
export const _startV8Coverage: BrowserCommand<[]> = async (context) => {
5+
const session: BrowserServerCDPHandler = await context.__ensureCDPHandler()
6+
await session.send('Profiler.enable')
7+
await session.send('Profiler.startPreciseCoverage', {
8+
callCount: true,
9+
detailed: true,
10+
})
11+
}
12+
13+
export const _takeV8Coverage: BrowserCommand<[]> = async (context) => {
14+
const session: BrowserServerCDPHandler = await context.__ensureCDPHandler()
15+
return session.send('Profiler.takePreciseCoverage')
16+
}

packages/browser/src/node/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { clear } from './clear'
22
import { click, dblClick, tripleClick } from './click'
3+
import { _startV8Coverage, _takeV8Coverage } from './coverage'
34
import { dragAndDrop } from './dragAndDrop'
45
import { fill } from './fill'
56
import {
@@ -22,6 +23,8 @@ export default {
2223
removeFile: removeFile as typeof removeFile,
2324
writeFile: writeFile as typeof writeFile,
2425
__vitest_fileInfo: _fileInfo as typeof _fileInfo,
26+
__vitest_startV8Coverage: _startV8Coverage as typeof _startV8Coverage,
27+
__vitest_takeV8Coverage: _takeV8Coverage as typeof _takeV8Coverage,
2528
__vitest_upload: upload as typeof upload,
2629
__vitest_click: click as typeof click,
2730
__vitest_dblClick: dblClick as typeof dblClick,

packages/browser/src/node/providers/playwright.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -404,19 +404,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
404404
const page = this.getPage(sessionid)
405405
const cdp = await page.context().newCDPSession(page)
406406
return {
407-
async send(method: string, params: any) {
408-
const result = await cdp.send(method as 'DOM.querySelector', params)
409-
return result as unknown
410-
},
411-
on(event: string, listener: (...args: any[]) => void) {
412-
cdp.on(event as 'Accessibility.loadComplete', listener)
413-
},
414-
off(event: string, listener: (...args: any[]) => void) {
415-
cdp.off(event as 'Accessibility.loadComplete', listener)
416-
},
417-
once(event: string, listener: (...args: any[]) => void) {
418-
cdp.once(event as 'Accessibility.loadComplete', listener)
419-
},
407+
send: cdp.send.bind(cdp) as CDPSession['send'],
408+
on: cdp.on.bind(cdp) as CDPSession['on'],
409+
off: cdp.off.bind(cdp) as CDPSession['off'],
410+
once: cdp.once.bind(cdp) as CDPSession['once'],
420411
}
421412
}
422413

packages/browser/src/node/rpc.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
128128
)
129129
}
130130

131+
function isCdpAllowed(project: TestProject) {
132+
return (
133+
project.config.api.allowExec
134+
&& project.config.browser.api.allowExec
135+
&& project.vitest.config.api.allowExec
136+
&& project.vitest.config.browser.api.allowExec
137+
&& project.config.api.allowWrite
138+
&& project.config.browser.api.allowWrite
139+
&& project.vitest.config.api.allowWrite
140+
&& project.vitest.config.browser.api.allowWrite
141+
)
142+
}
143+
144+
function assertCdpAllowed(project: TestProject) {
145+
if (!isCdpAllowed(project)) {
146+
throw new Error(
147+
`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.`,
148+
)
149+
}
150+
}
151+
131152
function setupClient(project: TestProject, rpcId: string, ws: WebSocket) {
132153
const mockResolver = new ServerMockResolver(globalServer.vite, {
133154
moduleDirectories: project.config.server?.deps?.moduleDirectories,
@@ -271,6 +292,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
271292
provider,
272293
contextId: sessionId,
273294
sessionId,
295+
__ensureCDPHandler: () => globalServer.ensureCDPHandler(sessionId, rpcId),
274296
},
275297
provider.getCommandsContext(sessionId),
276298
) as any as BrowserCommandContext
@@ -344,10 +366,12 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
344366

345367
// CDP
346368
async sendCdpEvent(sessionId: string, event: string, payload?: Record<string, unknown>) {
369+
assertCdpAllowed(project)
347370
const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId)
348371
return cdp.send(event, payload)
349372
},
350373
async trackCdpEvent(sessionId: string, type: 'on' | 'once' | 'off', event: string, listenerId: string) {
374+
assertCdpAllowed(project)
351375
const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId)
352376
cdp[type](event, listenerId)
353377
},

packages/coverage-v8/src/browser.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import type { Profiler } from 'node:inspector'
12
import type { CoverageProviderModule } from 'vitest/node'
23
import type { V8CoverageProvider } from './provider'
3-
import { cdp } from '@vitest/browser/context'
44
import { loadProvider } from './load-provider'
55

6-
const session = cdp()
76
let enabled = false
87

9-
type ScriptCoverage = Awaited<ReturnType<typeof session.send<'Profiler.takePreciseCoverage'>>>
8+
type ScriptCoverage = Profiler.TakePreciseCoverageReturnType
9+
10+
function triggerCommand(command: string, args: any[] = []): Promise<any> {
11+
return (globalThis as any).__vitest_browser_runner__.commands.triggerCommand(command, args)
12+
}
1013

1114
const mod: CoverageProviderModule = {
1215
async startCoverage() {
@@ -16,15 +19,11 @@ const mod: CoverageProviderModule = {
1619

1720
enabled = true
1821

19-
await session.send('Profiler.enable')
20-
await session.send('Profiler.startPreciseCoverage', {
21-
callCount: true,
22-
detailed: true,
23-
})
22+
await triggerCommand('__vitest_startV8Coverage')
2423
},
2524

2625
async takeCoverage(): Promise<{ result: any[] }> {
27-
const coverage = await session.send('Profiler.takePreciseCoverage')
26+
const coverage: ScriptCoverage = await triggerCommand('__vitest_takeV8Coverage')
2827
const result: typeof coverage.result = []
2928

3029
// Reduce amount of data sent over rpc by doing some early result filtering

packages/vitest/src/node/types/browser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ export interface BrowserCommandContext {
245245
/** @deprecated use `sessionId` instead */
246246
contextId: string
247247
sessionId: string
248+
/**
249+
* Returns Vitest's cached CDP handler for the current tester RPC connection.
250+
*
251+
* @internal
252+
*/
253+
__ensureCDPHandler: () => Promise<any>
248254
}
249255

250256
export interface BrowserServerStateSession {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { cdp } from '@vitest/browser/context'
2+
import { test } from 'vitest'
3+
4+
test('cdp throws an error', async () => {
5+
await cdp().send('Runtime.evaluate', { expression: '1 + 1' })
6+
})

0 commit comments

Comments
 (0)