diff --git a/scripts/hooks/insaits-security-wrapper.js b/scripts/hooks/insaits-security-wrapper.js index 21d168bc..710d9be8 100644 --- a/scripts/hooks/insaits-security-wrapper.js +++ b/scripts/hooks/insaits-security-wrapper.js @@ -15,6 +15,7 @@ const path = require('path'); const { spawnSync } = require('child_process'); const MAX_STDIN = 1024 * 1024; +const WINDOWS_SHELL_UNSAFE_PATH_CHARS = /[&|<>^%!]/; function isEnabled(value) { return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase()); @@ -37,17 +38,33 @@ process.stdin.on('end', () => { const scriptDir = __dirname; const pyScript = path.join(scriptDir, 'insaits-security-monitor.py'); - // Try python3 first (macOS/Linux), fall back to python (Windows) - const pythonCandidates = ['python3', 'python']; + // Prefer real Windows executables before .cmd shims so shell execution is + // only used for wrapper scripts such as pyenv/npm-style shims. + const pythonCandidates = process.platform === 'win32' + ? ['python3.exe', 'python.exe', 'python3.cmd', 'python.cmd', 'python3', 'python'] + : ['python3', 'python']; let result; for (const pythonBin of pythonCandidates) { + const useWindowsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(pythonBin); + if (useWindowsShell && ( + WINDOWS_SHELL_UNSAFE_PATH_CHARS.test(pythonBin) + || WINDOWS_SHELL_UNSAFE_PATH_CHARS.test(pyScript) + )) { + result = { + error: new Error(`Unsafe Windows Python shim path: ${pythonBin}`), + }; + break; + } + result = spawnSync(pythonBin, [pyScript], { input: raw, encoding: 'utf8', env: process.env, cwd: process.cwd(), timeout: 14000, + shell: useWindowsShell, + windowsHide: true, }); // ENOENT means binary not found - try next candidate diff --git a/tests/hooks/insaits-security-wrapper.test.js b/tests/hooks/insaits-security-wrapper.test.js index 26eea607..4d5c3401 100644 --- a/tests/hooks/insaits-security-wrapper.test.js +++ b/tests/hooks/insaits-security-wrapper.test.js @@ -21,8 +21,40 @@ function cleanup(dirPath) { } function writeFakePython(binDir) { - const fakePython = path.join(binDir, 'python3'); fs.mkdirSync(binDir, { recursive: true }); + if (process.platform === 'win32') { + const fakePythonJs = path.join(binDir, 'fake-python.js'); + const fakePythonCmd = path.join(binDir, 'python3.cmd'); + fs.writeFileSync(fakePythonJs, [ + "'use strict';", + "const fs = require('fs');", + "const mode = process.env.FAKE_INSAITS_MODE || 'clean';", + "if (mode === 'clean') {", + " fs.readFileSync(0, 'utf8');", + " process.exit(0);", + "}", + "if (mode === 'echo') {", + " process.stdout.write(fs.readFileSync(0, 'utf8'));", + " process.exit(0);", + "}", + "if (mode === 'block') {", + " process.stdout.write('blocked by monitor\\n');", + " process.stderr.write('monitor warning\\n');", + " process.exit(2);", + "}", + "if (mode === 'error') {", + " process.stderr.write('spawned but failed\\n');", + " process.exit(1);", + "}", + ].join('\n'), 'utf8'); + fs.writeFileSync(fakePythonCmd, [ + '@echo off', + `"${process.execPath}" "%~dp0fake-python.js" %*`, + ].join('\r\n'), 'utf8'); + return; + } + + const fakePython = path.join(binDir, 'python3'); fs.writeFileSync(fakePython, [ '#!/bin/sh', 'mode="${FAKE_INSAITS_MODE:-clean}"',