mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
* fix: handle array paths from VSCode terminal profiles - Updated terminal profile interfaces to support string | string[] for path property - Added normalizeShellPath helper to safely extract first element from array paths - Modified isShellAllowed to handle both string and array inputs - Updated getWindowsShellFromVSCode, getMacShellFromVSCode, and getLinuxShellFromVSCode to use normalizeShellPath - Added comprehensive tests for array path handling Fixes #7695 * feat: add validateShellPath export for robust shell validation - Created validateShellPath as a public API for shell path validation - Refactored internal validation logic into isShellAllowedInternal - Added comprehensive test coverage for all edge cases - Maintains backward compatibility with deprecated isShellAllowed - Handles arrays, strings, null, undefined, and nested arrays gracefully * Simplify roomote's work a little --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
370 lines
9.7 KiB
TypeScript
370 lines
9.7 KiB
TypeScript
import * as vscode from "vscode"
|
||
import { userInfo } from "os"
|
||
import * as path from "path"
|
||
|
||
// Security: Allowlist of approved shell executables to prevent arbitrary command execution
|
||
const SHELL_ALLOWLIST = new Set<string>([
|
||
// Windows PowerShell variants
|
||
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
|
||
"C:\\Program Files\\PowerShell\\6\\pwsh.exe",
|
||
"C:\\Program Files\\PowerShell\\5\\pwsh.exe",
|
||
|
||
// Windows Command Prompt
|
||
"C:\\Windows\\System32\\cmd.exe",
|
||
|
||
// Windows WSL
|
||
"C:\\Windows\\System32\\wsl.exe",
|
||
|
||
// Git Bash on Windows
|
||
"C:\\Program Files\\Git\\bin\\bash.exe",
|
||
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
||
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
||
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
|
||
|
||
// MSYS2/MinGW/Cygwin on Windows
|
||
"C:\\msys64\\usr\\bin\\bash.exe",
|
||
"C:\\msys32\\usr\\bin\\bash.exe",
|
||
"C:\\MinGW\\msys\\1.0\\bin\\bash.exe",
|
||
"C:\\cygwin64\\bin\\bash.exe",
|
||
"C:\\cygwin\\bin\\bash.exe",
|
||
|
||
// Unix/Linux/macOS - Bourne-compatible shells
|
||
"/bin/sh",
|
||
"/usr/bin/sh",
|
||
"/bin/bash",
|
||
"/usr/bin/bash",
|
||
"/usr/local/bin/bash",
|
||
"/opt/homebrew/bin/bash",
|
||
"/opt/local/bin/bash",
|
||
|
||
// Z Shell
|
||
"/bin/zsh",
|
||
"/usr/bin/zsh",
|
||
"/usr/local/bin/zsh",
|
||
"/opt/homebrew/bin/zsh",
|
||
"/opt/local/bin/zsh",
|
||
|
||
// Dash
|
||
"/bin/dash",
|
||
"/usr/bin/dash",
|
||
|
||
// Ash
|
||
"/bin/ash",
|
||
"/usr/bin/ash",
|
||
|
||
// C Shells
|
||
"/bin/csh",
|
||
"/usr/bin/csh",
|
||
"/bin/tcsh",
|
||
"/usr/bin/tcsh",
|
||
"/usr/local/bin/tcsh",
|
||
|
||
// Korn Shells
|
||
"/bin/ksh",
|
||
"/usr/bin/ksh",
|
||
"/bin/ksh93",
|
||
"/usr/bin/ksh93",
|
||
"/bin/mksh",
|
||
"/usr/bin/mksh",
|
||
"/bin/pdksh",
|
||
"/usr/bin/pdksh",
|
||
|
||
// Fish Shell
|
||
"/usr/bin/fish",
|
||
"/usr/local/bin/fish",
|
||
"/opt/homebrew/bin/fish",
|
||
"/opt/local/bin/fish",
|
||
|
||
// Modern shells
|
||
"/usr/bin/elvish",
|
||
"/usr/local/bin/elvish",
|
||
"/usr/bin/xonsh",
|
||
"/usr/local/bin/xonsh",
|
||
"/usr/bin/nu",
|
||
"/usr/local/bin/nu",
|
||
"/usr/bin/nushell",
|
||
"/usr/local/bin/nushell",
|
||
"/usr/bin/ion",
|
||
"/usr/local/bin/ion",
|
||
|
||
// BusyBox
|
||
"/bin/busybox",
|
||
"/usr/bin/busybox",
|
||
])
|
||
|
||
const SHELL_PATHS = {
|
||
// Windows paths
|
||
POWERSHELL_7: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
|
||
POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||
CMD: "C:\\Windows\\System32\\cmd.exe",
|
||
WSL_BASH: "/bin/bash",
|
||
// Unix paths
|
||
MAC_DEFAULT: "/bin/zsh",
|
||
LINUX_DEFAULT: "/bin/bash",
|
||
CSH: "/bin/csh",
|
||
BASH: "/bin/bash",
|
||
KSH: "/bin/ksh",
|
||
SH: "/bin/sh",
|
||
ZSH: "/bin/zsh",
|
||
DASH: "/bin/dash",
|
||
TCSH: "/bin/tcsh",
|
||
FALLBACK: "/bin/sh",
|
||
} as const
|
||
|
||
interface MacTerminalProfile {
|
||
path?: string | string[]
|
||
}
|
||
|
||
type MacTerminalProfiles = Record<string, MacTerminalProfile>
|
||
|
||
interface WindowsTerminalProfile {
|
||
path?: string | string[]
|
||
source?: "PowerShell" | "WSL"
|
||
}
|
||
|
||
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
|
||
|
||
interface LinuxTerminalProfile {
|
||
path?: string | string[]
|
||
}
|
||
|
||
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
|
||
|
||
// -----------------------------------------------------
|
||
// 1) VS Code Terminal Configuration Helpers
|
||
// -----------------------------------------------------
|
||
|
||
function getWindowsTerminalConfig() {
|
||
try {
|
||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||
const defaultProfileName = config.get<string>("defaultProfile.windows")
|
||
const profiles = config.get<WindowsTerminalProfiles>("profiles.windows") || {}
|
||
return { defaultProfileName, profiles }
|
||
} catch {
|
||
return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles }
|
||
}
|
||
}
|
||
|
||
function getMacTerminalConfig() {
|
||
try {
|
||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||
const defaultProfileName = config.get<string>("defaultProfile.osx")
|
||
const profiles = config.get<MacTerminalProfiles>("profiles.osx") || {}
|
||
return { defaultProfileName, profiles }
|
||
} catch {
|
||
return { defaultProfileName: null, profiles: {} as MacTerminalProfiles }
|
||
}
|
||
}
|
||
|
||
function getLinuxTerminalConfig() {
|
||
try {
|
||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||
const defaultProfileName = config.get<string>("defaultProfile.linux")
|
||
const profiles = config.get<LinuxTerminalProfiles>("profiles.linux") || {}
|
||
return { defaultProfileName, profiles }
|
||
} catch {
|
||
return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles }
|
||
}
|
||
}
|
||
|
||
// -----------------------------------------------------
|
||
// 2) Platform-Specific VS Code Shell Retrieval
|
||
// -----------------------------------------------------
|
||
|
||
/**
|
||
* Normalizes a path that can be either a string or an array of strings.
|
||
* If it's an array, returns the first element. Otherwise returns the string.
|
||
*/
|
||
function normalizeShellPath(path: string | string[] | undefined): string | null {
|
||
if (!path) return null
|
||
if (Array.isArray(path)) {
|
||
return path.length > 0 ? path[0] : null
|
||
}
|
||
return path
|
||
}
|
||
|
||
/** Attempts to retrieve a shell path from VS Code config on Windows. */
|
||
function getWindowsShellFromVSCode(): string | null {
|
||
const { defaultProfileName, profiles } = getWindowsTerminalConfig()
|
||
if (!defaultProfileName) {
|
||
return null
|
||
}
|
||
|
||
const profile = profiles[defaultProfileName]
|
||
|
||
// If the profile name indicates PowerShell, do version-based detection.
|
||
// In testing it was found these typically do not have a path, and this
|
||
// implementation manages to deductively get the correct version of PowerShell
|
||
if (defaultProfileName.toLowerCase().includes("powershell")) {
|
||
const normalizedPath = normalizeShellPath(profile?.path)
|
||
if (normalizedPath) {
|
||
// If there's an explicit PowerShell path, return that
|
||
return normalizedPath
|
||
} else if (profile?.source === "PowerShell") {
|
||
// If the profile is sourced from PowerShell, assume the newest
|
||
return SHELL_PATHS.POWERSHELL_7
|
||
}
|
||
// Otherwise, assume legacy Windows PowerShell
|
||
return SHELL_PATHS.POWERSHELL_LEGACY
|
||
}
|
||
|
||
// If there's a specific path, return that immediately
|
||
const normalizedPath = normalizeShellPath(profile?.path)
|
||
if (normalizedPath) {
|
||
return normalizedPath
|
||
}
|
||
|
||
// If the profile indicates WSL
|
||
if (profile?.source === "WSL" || defaultProfileName.toLowerCase().includes("wsl")) {
|
||
return SHELL_PATHS.WSL_BASH
|
||
}
|
||
|
||
// If nothing special detected, we assume cmd
|
||
return SHELL_PATHS.CMD
|
||
}
|
||
|
||
/** Attempts to retrieve a shell path from VS Code config on macOS. */
|
||
function getMacShellFromVSCode(): string | null {
|
||
const { defaultProfileName, profiles } = getMacTerminalConfig()
|
||
if (!defaultProfileName) {
|
||
return null
|
||
}
|
||
|
||
const profile = profiles[defaultProfileName]
|
||
return normalizeShellPath(profile?.path)
|
||
}
|
||
|
||
/** Attempts to retrieve a shell path from VS Code config on Linux. */
|
||
function getLinuxShellFromVSCode(): string | null {
|
||
const { defaultProfileName, profiles } = getLinuxTerminalConfig()
|
||
if (!defaultProfileName) {
|
||
return null
|
||
}
|
||
|
||
const profile = profiles[defaultProfileName]
|
||
return normalizeShellPath(profile?.path)
|
||
}
|
||
|
||
// -----------------------------------------------------
|
||
// 3) General Fallback Helpers
|
||
// -----------------------------------------------------
|
||
|
||
/**
|
||
* Tries to get a user’s shell from os.userInfo() (works on Unix if the
|
||
* underlying system call is supported). Returns null on error or if not found.
|
||
*/
|
||
function getShellFromUserInfo(): string | null {
|
||
try {
|
||
const { shell } = userInfo()
|
||
return shell || null
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/** Returns the environment-based shell variable, or null if not set. */
|
||
function getShellFromEnv(): string | null {
|
||
const { env } = process
|
||
|
||
if (process.platform === "win32") {
|
||
// On Windows, COMSPEC typically holds cmd.exe
|
||
return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe"
|
||
}
|
||
|
||
if (process.platform === "darwin") {
|
||
// On macOS/Linux, SHELL is commonly the environment variable
|
||
return env.SHELL || "/bin/zsh"
|
||
}
|
||
|
||
if (process.platform === "linux") {
|
||
// On Linux, SHELL is commonly the environment variable
|
||
return env.SHELL || "/bin/bash"
|
||
}
|
||
return null
|
||
}
|
||
|
||
// -----------------------------------------------------
|
||
// 4) Shell Validation Functions
|
||
// -----------------------------------------------------
|
||
|
||
/**
|
||
* Validates if a shell path is in the allowlist to prevent arbitrary command execution
|
||
*/
|
||
function isShellAllowed(shellPath: string): boolean {
|
||
if (!shellPath) return false
|
||
|
||
const normalizedPath = path.normalize(shellPath)
|
||
|
||
// Direct lookup first
|
||
if (SHELL_ALLOWLIST.has(normalizedPath)) {
|
||
return true
|
||
}
|
||
|
||
// On Windows, try case-insensitive comparison
|
||
if (process.platform === "win32") {
|
||
const lowerPath = normalizedPath.toLowerCase()
|
||
for (const allowedPath of SHELL_ALLOWLIST) {
|
||
if (allowedPath.toLowerCase() === lowerPath) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Returns a safe fallback shell based on the platform
|
||
*/
|
||
function getSafeFallbackShell(): string {
|
||
if (process.platform === "win32") {
|
||
return SHELL_PATHS.CMD
|
||
} else if (process.platform === "darwin") {
|
||
return SHELL_PATHS.MAC_DEFAULT
|
||
} else {
|
||
return SHELL_PATHS.LINUX_DEFAULT
|
||
}
|
||
}
|
||
|
||
// -----------------------------------------------------
|
||
// 5) Publicly Exposed Shell Getter
|
||
// -----------------------------------------------------
|
||
|
||
export function getShell(): string {
|
||
let shell: string | null = null
|
||
|
||
// 1. Check VS Code config first.
|
||
if (process.platform === "win32") {
|
||
// Special logic for Windows
|
||
shell = getWindowsShellFromVSCode()
|
||
} else if (process.platform === "darwin") {
|
||
// macOS from VS Code
|
||
shell = getMacShellFromVSCode()
|
||
} else if (process.platform === "linux") {
|
||
// Linux from VS Code
|
||
shell = getLinuxShellFromVSCode()
|
||
}
|
||
|
||
// 2. If no shell from VS Code, try userInfo()
|
||
if (!shell) {
|
||
shell = getShellFromUserInfo()
|
||
}
|
||
|
||
// 3. If still nothing, try environment variable
|
||
if (!shell) {
|
||
shell = getShellFromEnv()
|
||
}
|
||
|
||
// 4. Finally, fall back to a default
|
||
if (!shell) {
|
||
shell = getSafeFallbackShell()
|
||
}
|
||
|
||
// 5. Validate the shell against allowlist
|
||
if (!isShellAllowed(shell)) {
|
||
shell = getSafeFallbackShell()
|
||
}
|
||
|
||
return shell
|
||
}
|