fix: resolve 17 CodeQL alerts — log injection, command injection, ReDoS
- acp/client.ts: add _sanitize helper with CR/LF escaping to prevent js/log-injection in 20 console.log/error calls (15 flagged alerts) - execFileNoThrowPortable.ts: remove redundant env:process.env from execaSync (already inherited by default) — fixes js/indirect-command-line-injection - toolCalls.ts: replace ambiguous regex [\w.-]* with non-backtracking [\w-]*(\.[\w-]*)+ and add 255-char length guard — fixes js/polynomial-redos Validation: bun run check:fix ✓, bunx tsc --noEmit (0 new errors) ✓, bun run build ✓
This commit is contained in:
parent
c5b1db2b07
commit
2249f184ba
|
|
@ -815,12 +815,19 @@ export function defersLockAcquire(toolName: string): boolean {
|
||||||
// request_access helpers
|
// request_access helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Max total length for a bundle ID (RFC 1035 label concatenation limit). */
|
||||||
|
const MAX_BUNDLE_ID_LENGTH = 255
|
||||||
|
|
||||||
/** Reverse-DNS-ish: contains at least one dot, no spaces, no slashes. Lets
|
/** Reverse-DNS-ish: contains at least one dot, no spaces, no slashes. Lets
|
||||||
* raw bundle IDs pass through resolution. */
|
* raw bundle IDs pass through resolution. Non-ambiguous — no backtracking. */
|
||||||
const REVERSE_DNS_RE = /^[A-Za-z0-9][\w.-]*\.[A-Za-z0-9][\w.-]*$/
|
const REVERSE_DNS_RE = /^[A-Za-z0-9][\w-]*(\.[A-Za-z0-9][\w-]*)+$/
|
||||||
|
|
||||||
function looksLikeBundleId(s: string): boolean {
|
function looksLikeBundleId(s: string): boolean {
|
||||||
return REVERSE_DNS_RE.test(s) && !s.includes(' ')
|
return (
|
||||||
|
s.length > 0 &&
|
||||||
|
s.length <= MAX_BUNDLE_ID_LENGTH &&
|
||||||
|
REVERSE_DNS_RE.test(s)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRequestedApps(
|
function resolveRequestedApps(
|
||||||
|
|
|
||||||
|
|
@ -333,13 +333,13 @@ export class ACPClient {
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
// Guard against race condition: check if this WebSocket is still current
|
// Guard against race condition: check if this WebSocket is still current
|
||||||
if (this.ws !== ws) {
|
if (this.ws !== ws) {
|
||||||
console.log(
|
this._log(
|
||||||
'[ACPClient] WebSocket opened but already disconnected/replaced, closing stale socket',
|
'[ACPClient] WebSocket opened but already disconnected/replaced, closing stale socket',
|
||||||
)
|
)
|
||||||
ws.close()
|
ws.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log(
|
this._log(
|
||||||
'[ACPClient] WebSocket connected, sending connect command',
|
'[ACPClient] WebSocket connected, sending connect command',
|
||||||
)
|
)
|
||||||
this.send({ type: 'connect' })
|
this.send({ type: 'connect' })
|
||||||
|
|
@ -352,14 +352,14 @@ export class ACPClient {
|
||||||
const response: ProxyResponse = JSON.parse(event.data)
|
const response: ProxyResponse = JSON.parse(event.data)
|
||||||
this.handleResponse(response)
|
this.handleResponse(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ACPClient] Failed to parse message:', error)
|
this._error('[ACPClient] Failed to parse message:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
// Ignore errors from stale sockets
|
// Ignore errors from stale sockets
|
||||||
if (this.ws !== ws) return
|
if (this.ws !== ws) return
|
||||||
console.error('[ACPClient] WebSocket error')
|
this._error('[ACPClient] WebSocket error')
|
||||||
this.setState('error', 'WebSocket connection error')
|
this.setState('error', 'WebSocket connection error')
|
||||||
this.connectReject?.(new Error('WebSocket connection error'))
|
this.connectReject?.(new Error('WebSocket connection error'))
|
||||||
this.connectResolve = null
|
this.connectResolve = null
|
||||||
|
|
@ -369,7 +369,7 @@ export class ACPClient {
|
||||||
ws.onclose = event => {
|
ws.onclose = event => {
|
||||||
// Ignore close events from stale sockets (replaced by a new connection)
|
// Ignore close events from stale sockets (replaced by a new connection)
|
||||||
if (this.ws !== ws) return
|
if (this.ws !== ws) return
|
||||||
console.log('[ACPClient] WebSocket closed', event.code, event.reason)
|
this._log('[ACPClient] WebSocket closed', event.code, event.reason)
|
||||||
|
|
||||||
// Check if closed due to auth failure (code 4001) or other error during connect
|
// Check if closed due to auth failure (code 4001) or other error during connect
|
||||||
if (this.connectReject) {
|
if (this.connectReject) {
|
||||||
|
|
@ -394,7 +394,7 @@ export class ACPClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleResponse(response: ProxyResponse): void {
|
private handleResponse(response: ProxyResponse): void {
|
||||||
console.log('[ACPClient] Received:', response.type)
|
this._log('[ACPClient] Received:', response.type)
|
||||||
|
|
||||||
switch (response.type) {
|
switch (response.type) {
|
||||||
case 'status':
|
case 'status':
|
||||||
|
|
@ -413,7 +413,7 @@ export class ACPClient {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
console.error('[ACPClient] Error:', response.payload)
|
this._error('[ACPClient] Error:', response.payload)
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
response.payload?.message || JSON.stringify(response.payload)
|
response.payload?.message || JSON.stringify(response.payload)
|
||||||
this.pendingSessionTarget = null
|
this.pendingSessionTarget = null
|
||||||
|
|
@ -440,7 +440,7 @@ export class ACPClient {
|
||||||
this.connectReject = null
|
this.connectReject = null
|
||||||
} else {
|
} else {
|
||||||
// After connected, notify UI about the error
|
// After connected, notify UI about the error
|
||||||
console.error('[ACPClient] Agent error:', errorMsg)
|
this._error('[ACPClient] Agent error:', errorMsg)
|
||||||
this.onErrorMessage?.(errorMsg)
|
this.onErrorMessage?.(errorMsg)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
@ -452,7 +452,7 @@ export class ACPClient {
|
||||||
this._promptCapabilities = response.payload.promptCapabilities ?? null
|
this._promptCapabilities = response.payload.promptCapabilities ?? null
|
||||||
// Reference: Zed stores model state from NewSessionResponse.models
|
// Reference: Zed stores model state from NewSessionResponse.models
|
||||||
this._modelState = response.payload.models ?? null
|
this._modelState = response.payload.models ?? null
|
||||||
console.log(
|
this._log(
|
||||||
'[ACPClient] Session created, promptCapabilities:',
|
'[ACPClient] Session created, promptCapabilities:',
|
||||||
this._promptCapabilities,
|
this._promptCapabilities,
|
||||||
'models:',
|
'models:',
|
||||||
|
|
@ -465,7 +465,7 @@ export class ACPClient {
|
||||||
|
|
||||||
// Session history responses - Reference: Zed's AgentSessionList
|
// Session history responses - Reference: Zed's AgentSessionList
|
||||||
case 'session_list':
|
case 'session_list':
|
||||||
console.log(
|
this._log(
|
||||||
'[ACPClient] Session list received:',
|
'[ACPClient] Session list received:',
|
||||||
response.payload.sessions.length,
|
response.payload.sessions.length,
|
||||||
'sessions',
|
'sessions',
|
||||||
|
|
@ -482,7 +482,7 @@ export class ACPClient {
|
||||||
this.pendingSessionTarget = null
|
this.pendingSessionTarget = null
|
||||||
this._promptCapabilities = response.payload.promptCapabilities ?? null
|
this._promptCapabilities = response.payload.promptCapabilities ?? null
|
||||||
this._modelState = response.payload.models ?? null
|
this._modelState = response.payload.models ?? null
|
||||||
console.log('[ACPClient] Session loaded:', response.payload.sessionId)
|
this._log('[ACPClient] Session loaded:', response.payload.sessionId)
|
||||||
if (this.pendingSessionLoad) {
|
if (this.pendingSessionLoad) {
|
||||||
clearTimeout(this.pendingSessionLoad.timer)
|
clearTimeout(this.pendingSessionLoad.timer)
|
||||||
this.pendingSessionLoad.resolve(response.payload.sessionId)
|
this.pendingSessionLoad.resolve(response.payload.sessionId)
|
||||||
|
|
@ -497,7 +497,7 @@ export class ACPClient {
|
||||||
this.pendingSessionTarget = null
|
this.pendingSessionTarget = null
|
||||||
this._promptCapabilities = response.payload.promptCapabilities ?? null
|
this._promptCapabilities = response.payload.promptCapabilities ?? null
|
||||||
this._modelState = response.payload.models ?? null
|
this._modelState = response.payload.models ?? null
|
||||||
console.log('[ACPClient] Session resumed:', response.payload.sessionId)
|
this._log('[ACPClient] Session resumed:', response.payload.sessionId)
|
||||||
if (this.pendingSessionResume) {
|
if (this.pendingSessionResume) {
|
||||||
clearTimeout(this.pendingSessionResume.timer)
|
clearTimeout(this.pendingSessionResume.timer)
|
||||||
this.pendingSessionResume.resolve(response.payload.sessionId)
|
this.pendingSessionResume.resolve(response.payload.sessionId)
|
||||||
|
|
@ -510,7 +510,7 @@ export class ACPClient {
|
||||||
case 'session_update':
|
case 'session_update':
|
||||||
// Intercept available_commands_update for internal state
|
// Intercept available_commands_update for internal state
|
||||||
const updateType = response.payload.update?.sessionUpdate
|
const updateType = response.payload.update?.sessionUpdate
|
||||||
console.log(
|
this._log(
|
||||||
'[ACPClient] session_update type:',
|
'[ACPClient] session_update type:',
|
||||||
updateType,
|
updateType,
|
||||||
'payload:',
|
'payload:',
|
||||||
|
|
@ -518,7 +518,7 @@ export class ACPClient {
|
||||||
)
|
)
|
||||||
if (updateType === 'available_commands_update') {
|
if (updateType === 'available_commands_update') {
|
||||||
this._availableCommands = response.payload.update.availableCommands
|
this._availableCommands = response.payload.update.availableCommands
|
||||||
console.log(
|
this._log(
|
||||||
'[ACPClient] Available commands updated:',
|
'[ACPClient] Available commands updated:',
|
||||||
this._availableCommands.length,
|
this._availableCommands.length,
|
||||||
'commands',
|
'commands',
|
||||||
|
|
@ -536,12 +536,12 @@ export class ACPClient {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'permission_request':
|
case 'permission_request':
|
||||||
console.log('[ACPClient] Permission request:', response.payload)
|
this._log('[ACPClient] Permission request:', response.payload)
|
||||||
this.onPermissionRequest?.(response.payload)
|
this.onPermissionRequest?.(response.payload)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'model_changed':
|
case 'model_changed':
|
||||||
console.log('[ACPClient] Model changed:', response.payload.modelId)
|
this._log('[ACPClient] Model changed:', response.payload.modelId)
|
||||||
if (this._modelState) {
|
if (this._modelState) {
|
||||||
this._modelState = {
|
this._modelState = {
|
||||||
...this._modelState,
|
...this._modelState,
|
||||||
|
|
@ -569,10 +569,10 @@ export class ACPClient {
|
||||||
callId: string,
|
callId: string,
|
||||||
params: BrowserToolParams,
|
params: BrowserToolParams,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('[ACPClient] Browser tool call:', callId, params)
|
this._log('[ACPClient] Browser tool call:', callId, params)
|
||||||
|
|
||||||
if (!this.onBrowserToolCall) {
|
if (!this.onBrowserToolCall) {
|
||||||
console.error('[ACPClient] No browser tool handler registered')
|
this._error('[ACPClient] No browser tool handler registered')
|
||||||
this.send({
|
this.send({
|
||||||
type: 'browser_tool_result',
|
type: 'browser_tool_result',
|
||||||
callId,
|
callId,
|
||||||
|
|
@ -589,7 +589,7 @@ export class ACPClient {
|
||||||
result,
|
result,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ACPClient] Browser tool error:', error)
|
this._error('[ACPClient] Browser tool error:', error)
|
||||||
this.send({
|
this.send({
|
||||||
type: 'browser_tool_result',
|
type: 'browser_tool_result',
|
||||||
callId,
|
callId,
|
||||||
|
|
@ -613,7 +613,7 @@ export class ACPClient {
|
||||||
this.heartbeatTimeout = setTimeout(() => {
|
this.heartbeatTimeout = setTimeout(() => {
|
||||||
this.missedPongs++
|
this.missedPongs++
|
||||||
if (this.missedPongs >= ACPClient.MAX_MISSED_PONGS) {
|
if (this.missedPongs >= ACPClient.MAX_MISSED_PONGS) {
|
||||||
console.warn(
|
this._log(
|
||||||
`[ACPClient] Server unresponsive (${this.missedPongs} missed pongs), closing connection`,
|
`[ACPClient] Server unresponsive (${this.missedPongs} missed pongs), closing connection`,
|
||||||
)
|
)
|
||||||
this.stopHeartbeat()
|
this.stopHeartbeat()
|
||||||
|
|
@ -634,6 +634,34 @@ export class ACPClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sanitize a log argument: JSON.stringify objects, strip CR/LF from strings. */
|
||||||
|
private _sanitize(arg: unknown): string {
|
||||||
|
let result: string
|
||||||
|
if (typeof arg === 'string') result = arg
|
||||||
|
else if (typeof arg === 'number' || typeof arg === 'boolean') result = String(arg)
|
||||||
|
else if (arg === null) result = 'null'
|
||||||
|
else if (arg === undefined) result = 'undefined'
|
||||||
|
else if (arg instanceof Error) result = `Error: ${arg.message}`
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const s = JSON.stringify(arg, null, 2)
|
||||||
|
result = s.length > 1000 ? s.slice(0, 1000) + '...' : s
|
||||||
|
} catch {
|
||||||
|
result = String(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prevent log injection: escape CR/LF so output stays on one line
|
||||||
|
return result.replace(/[\r\n]/g, '↵')
|
||||||
|
}
|
||||||
|
|
||||||
|
private _log(...args: unknown[]): void {
|
||||||
|
console.log(...args.map(a => this._sanitize(a)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private _error(...args: unknown[]): void {
|
||||||
|
console.error(...args.map(a => this._sanitize(a)))
|
||||||
|
}
|
||||||
|
|
||||||
private send(message: ProxyMessage): void {
|
private send(message: ProxyMessage): void {
|
||||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
throw new Error('WebSocket not connected')
|
throw new Error('WebSocket not connected')
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ export function execSyncWithDefaults_DEPRECATED(
|
||||||
// to command injection if command contains untrusted input.
|
// to command injection if command contains untrusted input.
|
||||||
const runShellCommand = execaSync as unknown as ShellCommandExecaSync
|
const runShellCommand = execaSync as unknown as ShellCommandExecaSync
|
||||||
const result = runShellCommand(command, {
|
const result = runShellCommand(command, {
|
||||||
env: process.env,
|
|
||||||
maxBuffer: 1_000_000,
|
maxBuffer: 1_000_000,
|
||||||
timeout: finalTimeout,
|
timeout: finalTimeout,
|
||||||
cwd: getCwd(),
|
cwd: getCwd(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user