fix(claw,executor): restore Windows shim launch, close stray coroutine

- claw askClaude: prefer native .exe, route only .cmd/.bat through cmd.exe
  with a quoted command line (DEP0190-safe, same pattern as
  mcp-health-check); never exec .ps1 directly (not executable without
  powershell); model allowlist retained so the shell line carries no
  attacker metacharacters
- executor sync path: close un-awaited coroutine before returning the
  generic failure, eliminating RuntimeWarning noise
This commit is contained in:
Geronimo
2026-09-14 21:56:32 +05:30
parent a3c24818bb
commit 15f1ee8a41
2 changed files with 28 additions and 9 deletions
+26 -9
View File
@@ -95,29 +95,46 @@ function askClaude(systemPrompt, history, userMessage, model) {
}
args.push('-p');
// SECURITY: never use shell:true — on Windows Node concatenates command+args
// unquoted (DEP0190), so a model value like `x & calc &` breaks out.
// Validate the model token and spawn without a shell; resolve .cmd shim explicitly.
// SECURITY: a model value like `x & calc &` breaks out when Node
// concatenates command+args unquoted under cmd.exe (DEP0190), so the model
// token is validated and only fixed flags reach the command line.
if (model && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(model)) {
return `[Error: invalid model name]`;
}
// On Windows the `claude` binary is usually a .cmd shim, which Node
// >=18.20/20.12 refuses to spawn directly (CVE-2024-27980 mitigation), and
// .ps1 shims are not directly executable at all. Resolve a natively
// executable target first; only .cmd/.bat go through cmd.exe, using the
// same quoted-command-line pattern as scripts/hooks/mcp-health-check.js so
// space-containing paths survive as single tokens. .ps1 is never executed
// directly — fall through to bare `claude` (pre-change behavior) instead.
const quoteWin = token => (/[\s"&|<>^%!();]/.test(token) ? '"' + token.replace(/"/g, '""') + '"' : token);
let bin = 'claude';
let useShell = false;
if (process.platform === 'win32') {
for (const ext of ['.cmd', '.exe', '.ps1']) {
const { spawnSync: spawnWhere } = require('child_process');
for (const ext of ['.exe', '.cmd', '.bat']) {
let found = null;
try {
const found = require('child_process').spawnSync('where', [`claude${ext}`], { encoding: 'utf8' });
if (found.status === 0 && found.stdout.trim()) { bin = found.stdout.trim().split(/\r?\n/)[0]; break; }
found = spawnWhere('where', [`claude${ext}`], { encoding: 'utf8' });
} catch { /* ignore */ }
if (found && found.status === 0 && found.stdout && found.stdout.trim()) {
bin = found.stdout.trim().split(/\r?\n/)[0];
useShell = /\.(cmd|bat)$/i.test(bin);
break;
}
}
}
const result = spawnSync(bin, args, {
const spawnOpts = {
input: fullPrompt,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, CLAUDECODE: '' },
timeout: 300000,
shell: false
});
};
const result = useShell
? spawnSync([bin, ...args].map(quoteWin).join(' '), { ...spawnOpts, shell: true })
: spawnSync(bin, args, { ...spawnOpts, shell: false });
if (result.error) {
return `[Error: ${result.error.message}]`;