From 48d8fff3abddc7247bf7ac88ea82c9ebd6cd4edd Mon Sep 17 00:00:00 2001 From: root Date: Sun, 25 Jan 2026 07:37:46 +0000 Subject: [PATCH] fix: Eliminate command injection vulnerabilities in shell commands Replace shell-interpolated execSync calls with safer alternatives: - UpdateTabTitle: Use execFileSync with argument arrays for kitty/kitten - UpdateTabTitle: Use fetch() instead of curl for voice notifications - UpdateTabTitle: Write escape sequences directly to stderr - WhoisParser: Use execFile with input validation for whois queries - BountyPrograms: Use fs.mkdir instead of shell mkdir Co-Authored-By: Claude Opus 4.5 --- .../src/hooks/UpdateTabTitle.hook.ts | 63 ++++++++++++------- .../src/tools/BountyPrograms.ts | 6 +- .../pai-recon-skill/src/tools/WhoisParser.ts | 25 +++++++- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts b/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts index bfc0d2af6..82a6ae835 100755 --- a/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts +++ b/Packs/pai-hook-system/src/hooks/UpdateTabTitle.hook.ts @@ -60,7 +60,7 @@ * - Includes 5-second stdin timeout */ -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import { readFileSync } from 'fs'; import { inference } from '../skills/CORE/Tools/Inference'; import { isValidTabSummary, getTabFallback } from './lib/response-format'; @@ -227,38 +227,50 @@ function setTabTitle(title: string, state: TabState = 'normal'): void { // Add "…" suffix for active states const titleWithSuffix = state !== 'normal' ? `${title}…` : title; const truncated = titleWithSuffix.length > 50 ? titleWithSuffix.slice(0, 47) + '…' : titleWithSuffix; - const escaped = truncated.replace(/'/g, "'\\''"); // Check if we're in Kitty (TERM=xterm-kitty or KITTY_LISTEN_ON set) const isKitty = process.env.TERM === 'xterm-kitty' || process.env.KITTY_LISTEN_ON; if (isKitty) { // Use Kitty remote control - works even without TTY - execSync(`kitty @ set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 }); + // SECURITY: Use execFileSync with argument array to prevent command injection + execFileSync('kitty', ['@', 'set-tab-title', truncated], { stdio: 'ignore', timeout: 2000 }); // Set color based on state if (state === 'inference') { // Purple for inference/AI thinking - active tab stays dark blue, inactive shows purple - execSync( - `kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_INFERENCE_BG} inactive_fg=${INACTIVE_TEXT}`, - { stdio: 'ignore', timeout: 2000 } - ); + execFileSync('kitten', [ + '@', 'set-tab-color', '--self', + `active_bg=${ACTIVE_TAB_BG}`, + `active_fg=${ACTIVE_TEXT}`, + `inactive_bg=${TAB_INFERENCE_BG}`, + `inactive_fg=${INACTIVE_TEXT}` + ], { stdio: 'ignore', timeout: 2000 }); console.error('[UpdateTabTitle] Set inference color (purple on inactive only)'); } else if (state === 'working') { // Orange for actively working - active tab stays dark blue, inactive shows orange - execSync( - `kitten @ set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TEXT} inactive_bg=${TAB_WORKING_BG} inactive_fg=${INACTIVE_TEXT}`, - { stdio: 'ignore', timeout: 2000 } - ); + execFileSync('kitten', [ + '@', 'set-tab-color', '--self', + `active_bg=${ACTIVE_TAB_BG}`, + `active_fg=${ACTIVE_TEXT}`, + `inactive_bg=${TAB_WORKING_BG}`, + `inactive_fg=${INACTIVE_TEXT}` + ], { stdio: 'ignore', timeout: 2000 }); console.error('[UpdateTabTitle] Set working color (orange on inactive only)'); } console.error('[UpdateTabTitle] Set via Kitty remote control'); } else { // Fallback to escape codes for other terminals - execSync(`printf '\\033]0;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] }); - execSync(`printf '\\033]2;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] }); - execSync(`printf '\\033]30;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] }); + // SECURITY: Write escape sequences directly to stderr instead of using shell + const escapeSequences = [ + `\x1b]0;${truncated}\x07`, // Set window title + `\x1b]2;${truncated}\x07`, // Set window title (xterm) + `\x1b]30;${truncated}\x07` // Set tab title (Konsole) + ]; + for (const seq of escapeSequences) { + process.stderr.write(seq); + } } } catch (err) { console.error(`[UpdateTabTitle] Failed to set title: ${err}`); @@ -267,18 +279,25 @@ function setTabTitle(title: string, state: TabState = 'normal'): void { /** * Send voice notification + * SECURITY: Uses fetch() instead of shell-interpolated curl to prevent command injection */ -function announceVoice(summary: string): void { +async function announceVoice(summary: string): Promise { try { // Summary already starts with gerund - use directly, capitalize first letter const message = summary.charAt(0).toUpperCase() + summary.slice(1); - const escaped = message.replace(/"/g, '\\"'); - execSync( - `curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"message": "${escaped}"}' > /dev/null 2>&1 &`, - { stdio: 'ignore', timeout: 2000 } - ); + + // Use fetch with AbortController for timeout - no shell interpolation needed + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 2000); + + await fetch('http://localhost:8888/notify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message }), + signal: controller.signal + }).finally(() => clearTimeout(timeoutId)); } catch { - // Voice server might not be running + // Voice server might not be running - silent failure is expected } } @@ -324,7 +343,7 @@ async function main() { // Voice announcement - validated to prevent garbage if (isValidTabSummary(summary)) { - announceVoice(summary); + await announceVoice(summary); console.error(`[UpdateTabTitle] Voice: "${summary}"`); } else { console.error(`[UpdateTabTitle] Skipped voice - invalid summary: "${summary}"`); diff --git a/Packs/pai-recon-skill/src/tools/BountyPrograms.ts b/Packs/pai-recon-skill/src/tools/BountyPrograms.ts index 23ef2c4df..d78226950 100755 --- a/Packs/pai-recon-skill/src/tools/BountyPrograms.ts +++ b/Packs/pai-recon-skill/src/tools/BountyPrograms.ts @@ -20,7 +20,7 @@ * bun BountyPrograms.ts check example.com */ -import { $ } from "bun"; +import { mkdir } from "fs/promises"; interface BountyProgram { name: string; @@ -59,9 +59,9 @@ interface ChaosProgram { } async function ensureCacheDir(): Promise { - const dir = Bun.file(CACHE_DIR); try { - await $`mkdir -p ${CACHE_DIR}`.quiet(); + // SECURITY: Use fs.mkdir instead of shell interpolation + await mkdir(CACHE_DIR, { recursive: true }); } catch { // Directory might already exist } diff --git a/Packs/pai-recon-skill/src/tools/WhoisParser.ts b/Packs/pai-recon-skill/src/tools/WhoisParser.ts index 5bc03bdfc..2ae8749ad 100755 --- a/Packs/pai-recon-skill/src/tools/WhoisParser.ts +++ b/Packs/pai-recon-skill/src/tools/WhoisParser.ts @@ -10,7 +10,10 @@ * const ipInfo = await whoisIP("1.2.3.4"); */ -import { $ } from "bun"; +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); export interface DomainWhoisInfo { domain: string; @@ -54,13 +57,29 @@ export interface IPWhoisInfo { raw: string; } +/** + * Validate WHOIS query input to prevent injection + */ +function validateWhoisQuery(query: string): void { + // Only allow valid domain/IP characters + const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/; + if (!validPattern.test(query)) { + throw new Error(`Invalid WHOIS query: contains disallowed characters`); + } + if (query.length > 253) { + throw new Error(`Invalid WHOIS query: too long`); + } +} + /** * Execute WHOIS query + * SECURITY: Uses execFile with argument array to prevent command injection */ async function executeWhois(query: string): Promise { try { - const result = await $`whois ${query}`.text(); - return result; + validateWhoisQuery(query); + const { stdout } = await execFileAsync('whois', [query], { timeout: 30000 }); + return stdout; } catch (error) { throw new Error( `WHOIS query failed: ${error instanceof Error ? error.message : "Unknown error"}`