From 16a010fd21c66802cd6702047ee13030a1b1a8a6 Mon Sep 17 00:00:00 2001 From: myu003 Date: Wed, 18 Mar 2026 10:44:02 +0800 Subject: [PATCH] fix(extensions): harden Windows cmd launch in /diff and /files (#2329) closes #2288 --- .pi/extensions/diff.ts | 15 ++++++++++++++- .pi/extensions/files.ts | 14 +++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) 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(