···1010import { createBirpc } from 'birpc'
1111import { parse, stringify } from 'flatted'
1212import { dirname } from 'pathe'
1313-import { createDebugger, isFileServingAllowed } from 'vitest/node'
1313+import { createDebugger, isFileServingAllowed, isValidApiRequest } from 'vitest/node'
1414import { WebSocketServer } from 'ws'
15151616const debug = createDebugger('vitest:browser:api')
···30303131 const { pathname, searchParams } = new URL(request.url, 'http://localhost')
3232 if (pathname !== BROWSER_API_PATH) {
3333+ return
3434+ }
3535+3636+ if (!isValidApiRequest(vitest.config, request)) {
3737+ socket.destroy()
3338 return
3439 }
3540
+1
packages/browser/src/node/serverOrchestrator.ts
···4242 __VITEST_SESSION_ID__: JSON.stringify(sessionId),
4343 __VITEST_TESTER_ID__: '"none"',
4444 __VITEST_PROVIDED_CONTEXT__: '{}',
4545+ __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token),
4546 })
46474748 // disable CSP for the orchestrator as we are the ones controlling it
···11+import type { IncomingMessage } from 'node:http'
22+import type { ResolvedConfig } from '../node/types/config'
33+import crypto from 'node:crypto'
44+55+export function isValidApiRequest(config: ResolvedConfig, req: IncomingMessage): boolean {
66+ const url = new URL(req.url ?? '', 'http://localhost')
77+88+ // validate token. token is injected in ui/tester/orchestrator html, which is cross origin proteced.
99+ try {
1010+ const token = url.searchParams.get('token')
1111+ if (token && crypto.timingSafeEqual(
1212+ Buffer.from(token),
1313+ Buffer.from(config.api.token),
1414+ )) {
1515+ return true
1616+ }
1717+ }
1818+ // an error is thrown when the length is incorrect
1919+ catch {}
2020+2121+ return false
2222+}
+8-1
packages/vitest/src/api/setup.ts
···11import type { File, TaskResultPack } from '@vitest/runner'
2233+import type { IncomingMessage } from 'node:http'
34import type { ViteDevServer } from 'vite'
45import type { WebSocket } from 'ws'
56import type { Vitest } from '../node/core'
···2122import { getModuleGraph } from '../utils/graph'
2223import { stringifyReplace } from '../utils/serialization'
2324import { parseErrorStacktrace } from '../utils/source-map'
2525+import { isValidApiRequest } from './check'
24262527export function setup(ctx: Vitest, _server?: ViteDevServer) {
2628 const wss = new WebSocketServer({ noServer: true })
···29313032 const server = _server || ctx.server
31333232- server.httpServer?.on('upgrade', (request, socket, head) => {
3434+ server.httpServer?.on('upgrade', (request: IncomingMessage, socket, head) => {
3335 if (!request.url) {
3436 return
3537 }
36383739 const { pathname } = new URL(request.url, 'http://localhost')
3840 if (pathname !== API_PATH) {
4141+ return
4242+ }
4343+4444+ if (!isValidApiRequest(ctx.config, request)) {
4545+ socket.destroy()
3946 return
4047 }
4148
+1
packages/vitest/src/public/node.ts
···5566export const version = Vitest.version
7788+export { isValidApiRequest } from '../api/check'
89export { parseCLI } from '../node/cli/cac'
910export type { CliParseOptions } from '../node/cli/cac'
1011export { startVitest } from '../node/cli/cli-api'
···88} from '../types/config'
99import type { BaseCoverageOptions, CoverageReporterWithOptions } from '../types/coverage'
1010import type { BuiltinPool, ForksOptions, PoolOptions, ThreadsOptions } from '../types/pool-options'
1111+import crypto from 'node:crypto'
1112import { toArray } from '@vitest/utils'
1213import { resolveModule } from 'local-pkg'
1314import { normalize, relative, resolve } from 'pathe'
···629630 }
630631631632 // the server has been created, we don't need to override vite.server options
632632- resolved.api = resolveApiServerConfig(options, defaultPort)
633633+ const api = resolveApiServerConfig(options, defaultPort)
634634+ resolved.api = { ...api, token: crypto.randomUUID() }
633635634636 if (options.related) {
635637 resolved.related = toArray(options.related).map(file =>