Roo-Code/src/utils/shell.ts
roomote[bot] 2b533993ff
fix: handle array paths from VSCode terminal profiles (#7697)
* 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>
2025-09-05 11:41:00 -07:00

370 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 users 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
}