diff --git a/.pi/extensions/diff.ts b/.pi/extensions/diff.ts index 2a8e2e07..dcd426b4 100644 --- a/.pi/extensions/diff.ts +++ b/.pi/extensions/diff.ts @@ -66,9 +66,20 @@ export default function (pi: ExtensionAPI) { return; } + const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/; + const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`; + const openWithCode = async (file: string) => { if (process.platform === "win32") { - return pi.exec("cmd", ["/d", "/s", "/c", "code", "-g", file], { cwd: ctx.cwd }); + if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) { + ctx.ui.notify( + `Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`, + "error", + ); + return null; + } + const commandLine = `code -g ${quoteCmdArg(file)}`; + return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd }); } return pi.exec("code", ["-g", file], { cwd: ctx.cwd }); }; @@ -79,6 +90,7 @@ export default function (pi: ExtensionAPI) { // For untracked files, git difftool won't work, so fall back to just opening the file. if (fileInfo.status === "?") { const openResult = await openWithCode(fileInfo.file); + if (!openResult) return; if (openResult.code !== 0) { const openStderr = openResult.stderr.trim(); ctx.ui.notify( @@ -104,6 +116,7 @@ export default function (pi: ExtensionAPI) { ); const openResult = await openWithCode(fileInfo.file); + if (!openResult) return; if (openResult.code !== 0) { const openStderr = openResult.stderr.trim(); ctx.ui.notify( diff --git a/.pi/extensions/files.ts b/.pi/extensions/files.ts index 79c4bf0a..53d0b380 100644 --- a/.pi/extensions/files.ts +++ b/.pi/extensions/files.ts @@ -89,9 +89,20 @@ export default function (pi: ExtensionAPI) { // Sort by most recent first const files = Array.from(fileMap.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp); + const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/; + const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`; + const openWithCode = async (path: string) => { if (process.platform === "win32") { - return pi.exec("cmd", ["/d", "/s", "/c", "code", "-g", path], { cwd: ctx.cwd }); + if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(path)) { + ctx.ui.notify( + `Refusing to open ${path}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`, + "error", + ); + return null; + } + const commandLine = `code -g ${quoteCmdArg(path)}`; + return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd }); } return pi.exec("code", ["-g", path], { cwd: ctx.cwd }); }; @@ -99,6 +110,7 @@ export default function (pi: ExtensionAPI) { const openSelected = async (file: FileEntry): Promise => { try { const openResult = await openWithCode(file.path); + if (!openResult) return; if (openResult.code !== 0) { const openStderr = openResult.stderr.trim(); ctx.ui.notify(