Skip to content

Commit d78dffb

Browse files
committed
Security error fix
1 parent f8bad45 commit d78dffb

11 files changed

Lines changed: 188 additions & 172 deletions

File tree

packages/backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
"fastify": "^5.8.4",
3333
"get-port": "^7.1.0",
3434
"import-meta-resolve": "^4.1.0",
35+
"shell-quote": "^1.8.3",
3536
"tree-kill": "^1.2.2"
3637
},
3738
"devDependencies": {
39+
"@types/shell-quote": "^1.7.5",
3840
"@types/ws": "^8.18.1",
3941
"nodemon": "^3.1.14",
4042
"ws": "^8.18.3"

packages/backend/src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,17 @@ export async function start(
136136
)
137137
replayBufferedMessages(socket)
138138
clients.add(socket)
139-
socket.on('close', () => clients.delete(socket))
139+
socket.on('close', () => {
140+
clients.delete(socket)
141+
// Last dashboard window closed — tell the worker so it can wind
142+
// down. Lets the user close Chrome to end an interactive review
143+
// session under any runner.
144+
if (clients.size === 0 && workerSocket?.readyState === WebSocket.OPEN) {
145+
workerSocket.send(
146+
JSON.stringify({ scope: 'clientDisconnected', data: {} })
147+
)
148+
}
149+
})
140150

141151
if (workerSocket?.readyState === WebSocket.OPEN) {
142152
workerSocket.send(

packages/backend/src/runner.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path'
44
import url from 'node:url'
55
import { createRequire } from 'node:module'
66
import kill from 'tree-kill'
7+
import { parse as shellParse } from 'shell-quote'
78
import type { RunnerRequestBody } from './types.js'
89
import { WDIO_CONFIG_FILENAMES, NIGHTWATCH_CONFIG_FILENAMES } from './types.js'
910

@@ -188,12 +189,12 @@ class TestRunner {
188189
if (isGenericShell) {
189190
const command = this.#resolveGenericCommand(payload)
190191
this.#baseDir = process.env.DEVTOOLS_RUNNER_CWD || process.cwd()
191-
child = spawn(command, {
192+
const { file, args } = this.#parseGenericCommand(command)
193+
child = spawn(file, args, {
192194
cwd: this.#baseDir,
193195
env: childEnv,
194196
stdio: 'inherit',
195-
detached: false,
196-
shell: true
197+
detached: false
197198
})
198199
} else {
199200
const configPath = this.#resolveConfigPath(payload)
@@ -270,6 +271,17 @@ class TestRunner {
270271
return fallback || template || ''
271272
}
272273

274+
#parseGenericCommand(command: string): { file: string; args: string[] } {
275+
const tokens = (shellParse(command) as unknown[]).filter(
276+
(token): token is string => typeof token === 'string'
277+
)
278+
if (tokens.length === 0) {
279+
throw new Error('Invalid generic command: empty command')
280+
}
281+
const [file, ...args] = tokens
282+
return { file, args }
283+
}
284+
273285
stop() {
274286
if (!this.#child || !this.#child.pid) {
275287
return

packages/selenium-devtools/example/vitest-test/setup.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/selenium-devtools/example/vitest-test/test/example.js

Lines changed: 0 additions & 54 deletions
This file was deleted.

packages/selenium-devtools/example/vitest-test/vitest.config.js

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import net from 'node:net'
2+
import { spawn } from 'node:child_process'
3+
import { createRequire } from 'node:module'
4+
5+
const require = createRequire(import.meta.url)
6+
7+
export async function startDetachedBackend(opts: {
8+
port: number
9+
hostname: string
10+
readyTimeoutMs?: number
11+
}): Promise<{ port: number }> {
12+
const backendPath = require.resolve('@wdio/devtools-backend')
13+
const code = `import(${JSON.stringify(backendPath)}).then(m => m.start({ port: ${opts.port}, hostname: ${JSON.stringify(opts.hostname)} })).catch(err => { console.error(err); process.exit(1) })`
14+
spawn(process.execPath, ['-e', code], {
15+
detached: true,
16+
stdio: 'ignore'
17+
}).unref()
18+
19+
const deadline = Date.now() + (opts.readyTimeoutMs ?? 10000)
20+
while (Date.now() < deadline) {
21+
if (await canConnect(opts.port, opts.hostname)) {
22+
return { port: opts.port }
23+
}
24+
await new Promise((resolve) => setTimeout(resolve, 100))
25+
}
26+
throw new Error(
27+
`Detached backend never came up on ${opts.hostname}:${opts.port}`
28+
)
29+
}
30+
31+
function canConnect(port: number, host: string): Promise<boolean> {
32+
return new Promise((resolve) => {
33+
const sock = net.connect(port, host)
34+
sock.once('connect', () => {
35+
sock.destroy()
36+
resolve(true)
37+
})
38+
sock.once('error', () => {
39+
sock.destroy()
40+
resolve(false)
41+
})
42+
})
43+
}

0 commit comments

Comments
 (0)