diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 27f9b59..86d061f 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,3 +1,8 @@ +## 2026-07-17 - [CWD Binary Hijacking Mitigation for Electron Shell Process Spawning] +**Vulnerability:** Spawning a shell with relative binary names (e.g. `powershell.exe` or `cmd.exe`) on Windows or POSIX can lead to CWD Binary Hijacking if a workspace has a malicious executable with the same name. +**Learning:** Simply whitelisting simple shell names is not sufficient if the underlying process-spawning call resolves relative paths relative to the current working directory (`workspaceRoot`). Electron main processes must always resolve shell binaries to secure, absolute paths (e.g. in `System32` or `/bin`) before execution. +**Prevention:** Always map whitelisted shell names to absolute, safe system paths (e.g. `path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\cmd.exe')`) and spawn using those absolute paths only. + ## 2026-07-17 - [IPC Command and Process Injection Prevention in Electron] **Vulnerability:** Unsanitized parameters exposed via IPC handlers in Electron allowed the renderer process to trigger arbitrary process execution (`restart-shell` with arbitrary executable path) and git command/argument injection (`git-cmd` with custom arguments like `--ext-diff`). **Learning:** Even if context isolation is enabled and the renderer does not expose the raw `child_process` module, exposed high-level IPC handlers (like Git commands or custom shell selection handlers) can still be abused as a bridge for command/argument injection if they do not strictly sanitize or validate their inputs on the main process side. diff --git a/electron.cjs b/electron.cjs index fc38dad..3ac1796 100644 --- a/electron.cjs +++ b/electron.cjs @@ -150,7 +150,8 @@ app.on('activate', function () { function getShellPath() { const platform = os.platform(); if (platform === 'win32') { - return 'powershell.exe'; + const systemRoot = process.env.SystemRoot || 'C:\\Windows'; + return path.join(systemRoot, 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'); } else if (platform === 'darwin') { return process.env.SHELL || '/bin/zsh'; } else { @@ -678,6 +679,38 @@ function isSafeShell(shellPath) { return ALLOWED_SHELLS.includes(normalized); } +function getAbsoluteSafeShellPath(shellPath) { + if (typeof shellPath !== 'string') return getShellPath(); + const normalized = shellPath.trim().replace(/\\/g, '/').toLowerCase(); + const platform = os.platform(); + const systemRoot = process.env.SystemRoot || 'C:\\Windows'; + + if (platform === 'win32') { + if (normalized.includes('powershell')) { + return path.join(systemRoot, 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'); + } + if (normalized.includes('cmd')) { + return path.join(systemRoot, 'System32\\cmd.exe'); + } + if (normalized.includes('pwsh')) { + const p7 = path.join(process.env.ProgramFiles || 'C:\\Program Files', 'PowerShell\\7\\pwsh.exe'); + if (fs.existsSync(p7)) return p7; + return path.join(systemRoot, 'System32\\WindowsPowerShell\\v1.0\\powershell.exe'); + } + } else { + if (normalized.endsWith('bash')) { + return fs.existsSync('/bin/bash') ? '/bin/bash' : '/usr/bin/bash'; + } + if (normalized.endsWith('zsh')) { + return fs.existsSync('/bin/zsh') ? '/bin/zsh' : '/usr/bin/zsh'; + } + if (normalized.endsWith('sh')) { + return fs.existsSync('/bin/sh') ? '/bin/sh' : '/usr/bin/sh'; + } + } + return getShellPath(); +} + ipcMain.handle('restart-shell', (event, shellPath) => { if (shellPath) { // Secure validation to prevent process execution of arbitrary binaries (Command/Process Injection vulnerability) @@ -685,10 +718,12 @@ ipcMain.handle('restart-shell', (event, shellPath) => { console.warn(`Blocked unsafe shell override attempt: "${shellPath}"`); return false; } + // Convert to a secure absolute path to prevent CWD binary-hijacking or path-traversal bypasses + const secureShellPath = getAbsoluteSafeShellPath(shellPath); // Override the shell for this restart const savedShell = workspaceRoot; workspaceRoot = savedShell; - startNewShellWith(shellPath); + startNewShellWith(secureShellPath); } else { startNewShell(); }