[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(browser): disable client `cdp` API when `allowWrite/allowExec: false` [backport to v3] (#10456)

Co-authored-by: Codex <noreply@openai.com>

authored by

Hiroshi Ogawa
Codex
and committed by
GitHub
(May 28, 2026, 12:46 PM +0200) 385a1aef af88b1f5

+143 -24
+2
docs/guide/browser/commands.md
··· 59 59 60 60 ::: warning 61 61 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). 62 64 ::: 63 65 64 66 ## Custom Commands
+2 -2
docs/guide/browser/config.md
··· 155 155 - **Type:** `boolean` 156 156 - **Default:** inherited from [`api.allowWrite`](/config/#api-allowwrite) 157 157 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). 159 159 160 160 ### browser.api.allowExec {#browser-api-allowexec} 161 161 162 162 - **Type:** `boolean` 163 163 - **Default:** inherited from [`api.allowExec`](/config/#api-allowexec) 164 164 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). 166 166 167 167 ## browser.provider {#browser-provider} 168 168
+2
docs/guide/browser/context.md
··· 116 116 117 117 ::: warning 118 118 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). 119 121 ::: 120 122 121 123 ```ts
+8 -9
packages/coverage-v8/src/browser.ts
··· 1 + import type { Profiler } from 'node:inspector' 1 2 import type { CoverageProviderModule } from 'vitest/node' 2 3 import type { V8CoverageProvider } from './provider' 3 - import { cdp } from '@vitest/browser/context' 4 4 import { loadProvider } from './load-provider' 5 5 6 - const session = cdp() 7 6 let enabled = false 8 7 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 + } 10 13 11 14 const mod: CoverageProviderModule = { 12 15 async startCoverage() { ··· 16 19 17 20 enabled = true 18 21 19 - await session.send('Profiler.enable') 20 - await session.send('Profiler.startPreciseCoverage', { 21 - callCount: true, 22 - detailed: true, 23 - }) 22 + await triggerCommand('__vitest_startV8Coverage') 24 23 }, 25 24 26 25 async takeCoverage(): Promise<{ result: any[] }> { 27 - const coverage = await session.send('Profiler.takePreciseCoverage') 26 + const coverage: ScriptCoverage = await triggerCommand('__vitest_takeV8Coverage') 28 27 const result: typeof coverage.result = [] 29 28 30 29 // Reduce amount of data sent over rpc by doing some early result filtering
+14
test/browser/specs/errors-cdp.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { provider, runBrowserTests } from './utils' 3 + 4 + // v3 backport of 5 + // https://github.com/vitest-dev/vitest/blob/a88ab04c710d7703b3d48c74bec4cd6382077acc/test/browser/specs/errors.test.ts#L222 6 + test.runIf(provider === 'playwright')('cannot use cdp if write or exec is disabled', async () => { 7 + const { stderr } = await runBrowserTests({ 8 + root: './fixtures/errors-cdp', 9 + }) 10 + 11 + expect(stderr).toContain( 12 + 'Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.', 13 + ) 14 + })
+36
test/coverage-test/test/browser-api-permissions.browser.test.ts
··· 1 + import { expect } from 'vitest' 2 + import { readCoverageMap, runVitest, test } from '../utils' 3 + 4 + test('browser coverage works when browser api write and exec are disabled', async () => { 5 + await runVitest({ 6 + api: { 7 + allowExec: false, 8 + allowWrite: false, 9 + }, 10 + browser: { 11 + api: { 12 + allowExec: false, 13 + allowWrite: false, 14 + }, 15 + }, 16 + include: ['fixtures/test/math.test.ts'], 17 + coverage: { 18 + reporter: 'json', 19 + include: ['fixtures/src/math.ts'], 20 + }, 21 + }) 22 + 23 + const coverageMap = await readCoverageMap() 24 + const fileCoverages = coverageMap.files().map(file => coverageMap.fileCoverageFor(file)) 25 + 26 + expect(fileCoverages).toMatchInlineSnapshot(` 27 + { 28 + "<process-cwd>/fixtures/src/math.ts": { 29 + "branches": "0/0 (100%)", 30 + "functions": "1/4 (25%)", 31 + "lines": "1/4 (25%)", 32 + "statements": "1/4 (25%)", 33 + }, 34 + } 35 + `) 36 + })
+24
packages/browser/src/node/rpc.ts
··· 128 128 ) 129 129 } 130 130 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 + 131 152 function setupClient(project: TestProject, rpcId: string, ws: WebSocket) { 132 153 const mockResolver = new ServerMockResolver(globalServer.vite, { 133 154 moduleDirectories: project.config.server?.deps?.moduleDirectories, ··· 271 292 provider, 272 293 contextId: sessionId, 273 294 sessionId, 295 + __ensureCDPHandler: () => globalServer.ensureCDPHandler(sessionId, rpcId), 274 296 }, 275 297 provider.getCommandsContext(sessionId), 276 298 ) as any as BrowserCommandContext ··· 344 366 345 367 // CDP 346 368 async sendCdpEvent(sessionId: string, event: string, payload?: Record<string, unknown>) { 369 + assertCdpAllowed(project) 347 370 const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId) 348 371 return cdp.send(event, payload) 349 372 }, 350 373 async trackCdpEvent(sessionId: string, type: 'on' | 'once' | 'off', event: string, listenerId: string) { 374 + assertCdpAllowed(project) 351 375 const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId) 352 376 cdp[type](event, listenerId) 353 377 },
+6
test/browser/fixtures/errors-cdp/cdp.test.ts
··· 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 + })
+20
test/browser/fixtures/errors-cdp/vitest.config.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { defineConfig } from 'vitest/config' 3 + import { provider } from '../../settings' 4 + 5 + export default defineConfig({ 6 + cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)), 7 + test: { 8 + browser: { 9 + enabled: true, 10 + provider, 11 + instances: [{ browser: 'chromium' }], 12 + headless: true, 13 + screenshotFailures: false, 14 + api: { 15 + allowExec: false, 16 + allowWrite: false, 17 + }, 18 + }, 19 + }, 20 + })
+16
packages/browser/src/node/commands/coverage.ts
··· 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 + }
+3
packages/browser/src/node/commands/index.ts
··· 1 1 import { clear } from './clear' 2 2 import { click, dblClick, tripleClick } from './click' 3 + import { _startV8Coverage, _takeV8Coverage } from './coverage' 3 4 import { dragAndDrop } from './dragAndDrop' 4 5 import { fill } from './fill' 5 6 import { ··· 22 23 removeFile: removeFile as typeof removeFile, 23 24 writeFile: writeFile as typeof writeFile, 24 25 __vitest_fileInfo: _fileInfo as typeof _fileInfo, 26 + __vitest_startV8Coverage: _startV8Coverage as typeof _startV8Coverage, 27 + __vitest_takeV8Coverage: _takeV8Coverage as typeof _takeV8Coverage, 25 28 __vitest_upload: upload as typeof upload, 26 29 __vitest_click: click as typeof click, 27 30 __vitest_dblClick: dblClick as typeof dblClick,
+4 -13
packages/browser/src/node/providers/playwright.ts
··· 404 404 const page = this.getPage(sessionid) 405 405 const cdp = await page.context().newCDPSession(page) 406 406 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'], 420 411 } 421 412 } 422 413
+6
packages/vitest/src/node/types/browser.ts
··· 245 245 /** @deprecated use `sessionId` instead */ 246 246 contextId: string 247 247 sessionId: string 248 + /** 249 + * Returns Vitest's cached CDP handler for the current tester RPC connection. 250 + * 251 + * @internal 252 + */ 253 + __ensureCDPHandler: () => Promise<any> 248 254 } 249 255 250 256 export interface BrowserServerStateSession {