import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; /** * Validates that a path is strictly within the workspace. * Prevents Path Traversal attacks by resolving real paths and checking boundaries. */ /** * Additional trusted root paths beyond the workspace. * Populated once from VS Code workspace folders on first call. */ let _trustedRoots: string[] | null = null; function getTrustedRoots(workspaceRoot: string): string[] { if (_trustedRoots) return _trustedRoots; const roots = [path.normalize(workspaceRoot).toLowerCase()]; // Include all open workspace folders as trusted roots const folders = vscode.workspace.workspaceFolders; if (folders) { for (const f of folders) { roots.push(path.normalize(f.uri.fsPath).toLowerCase()); } } // Also trust the immediate parent of each root, so sibling projects under a // shared parent (e.g. E:\Wiki\connectai + E:\Wiki\Datacollect) are reachable // for read/list. Guard: never widen to a drive/filesystem root. for (const r of [...roots]) { const parent = path.normalize(path.dirname(r)).toLowerCase(); if (parent && parent !== r && path.dirname(parent) !== parent) { roots.push(parent); } } _trustedRoots = [...new Set(roots)]; return _trustedRoots; } /** Reset cached roots (useful when workspace folders change). */ export function resetTrustedRoots(): void { _trustedRoots = null; } export function validatePath(workspaceRoot: string, targetPath: string): string { if (!workspaceRoot) { throw new Error("Security Violation: Workspace root not defined."); } const absolutePath = path.resolve(workspaceRoot, targetPath); const normalizedTarget = path.normalize(absolutePath).toLowerCase(); const trusted = getTrustedRoots(workspaceRoot); const isTrusted = trusted.some(root => normalizedTarget.startsWith(root)); if (!isTrusted) { throw new Error(`Security Violation: Path traversal detected! Attempted to access ${absolutePath} which is outside allowed boundaries.`); } return absolutePath; } /** * Splits a command on top-level `&&`, ignoring `&&` that appears inside single- * or double-quoted strings (e.g. a commit message). Returns trimmed, non-empty parts. */ function splitTopLevelAnd(command: string): string[] { const parts: string[] = []; let buf = ''; let quote: string | null = null; for (let i = 0; i < command.length; i++) { const c = command[i]; if (quote) { buf += c; if (c === quote) { quote = null; } continue; } if (c === "'" || c === '"') { quote = c; buf += c; continue; } if (c === '&' && command[i + 1] === '&') { parts.push(buf); buf = ''; i++; // skip the second '&' continue; } buf += c; } parts.push(buf); return parts.map(p => p.trim()).filter(p => p.length > 0); } /** * Windows PowerShell 5.1 — the default VS Code integrated terminal on Windows — * does not support the `&&` chaining operator (it is a hard parser error, so the * WHOLE command fails to run). Local models emit `&&` constantly because every * git/npm tutorial uses it, and a system-prompt rule alone does not reliably * stop a small model. So rewrite `A && B && C` into a PowerShell-native * conditional chain that preserves short-circuit semantics: * * A && B && C -> A; if ($?) { B; if ($?) { C } } * * `$?` reflects the success of the previous command, so a failed step still * short-circuits the rest — important so e.g. a failed `cd` never lets `git` * run in the wrong directory. * * On macOS/Linux this rewrite is actively harmful — `&&` is the native * POSIX-shell operator and the PowerShell `if ($?) { ... }` shape is a zsh/bash * syntax error. So skip the rewrite entirely off-Windows. */ function rewriteForPowerShell(command: string): string { if (process.platform !== 'win32') { return command; } if (!command.includes('&&')) { return command; } const parts = splitTopLevelAnd(command); if (parts.length <= 1) { return command; } let chain = parts[parts.length - 1]; for (let i = parts.length - 2; i >= 0; i--) { chain = `${parts[i]}; if ($?) { ${chain} }`; } return chain; } /** * Sanitizes terminal commands to prevent destructive actions. * Uses a combination of blocklist for dangerous patterns and recommendation for allowed tools. */ export function sanitizeCommand(command: string): string { const trimmedCmd = command.trim(); // 1. Dangerous Shell Characters/Patterns (Blocklist) const dangerousPatterns = [ /rm\s+-rf\s+\//, // Root deletion /mkfs/, // Filesystem formatting /dd\s+if=/, // Low-level disk writing />\s*\/dev\/sd/, // Direct disk access /:(){:|:&};:/, // Fork bomb /shutdown/, // System shutdown /reboot/, // System reboot /mv\s+.*\/dev\/null/ // Moving to null ]; for (const pattern of dangerousPatterns) { if (pattern.test(trimmedCmd)) { throw new Error(`Security Violation: Destructive command pattern detected! Blocked: ${trimmedCmd}`); } } // 2. Allowlist of safe base commands (Optional but recommended) // For now, we allow common development tools const safeBaseCommands = [ 'npm', 'node', 'npx', 'git', 'python', 'python3', 'pip', 'pip3', 'cargo', 'rustc', 'go', 'gcc', 'g++', 'make', 'ls', 'cat', 'echo', 'mkdir', 'cp', 'mv', 'touch' ]; const baseCmd = trimmedCmd.split(/\s+/)[0]; if (baseCmd && !safeBaseCommands.includes(baseCmd)) { console.warn(`[Security] Warning: Running uncommon command '${baseCmd}'. Ensure this is intended.`); } // Rewrite `&&` chains for PowerShell (the Windows default terminal) so the // command actually runs instead of failing with a parser error. return rewriteForPowerShell(trimmedCmd); }