fix(hooks): add Windows .cmd support with shell injection guard

Handle Windows .cmd shim resolution via spawnSync with strict path
validation. Removes shell:true injection risk, uses strict equality,
and restores .cmd support with path injection guard.
This commit is contained in:
Jonghyeok Park
2026-03-10 22:37:57 +09:00
parent 66498ae9ac
commit 0a3afbe38f
4 changed files with 33 additions and 10 deletions

View File

@@ -17,9 +17,12 @@
* Fails silently if no formatter is found or installed.
*/
const { execFileSync } = require('child_process');
const { execFileSync, spawnSync } = require('child_process');
const path = require('path');
// Shell metacharacters that cmd.exe interprets as command separators/operators
const UNSAFE_PATH_CHARS = /[&|<>^%!]/;
const { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
const MAX_STDIN = 1024 * 1024; // 1MB limit
@@ -50,11 +53,29 @@ function run(rawInput) {
// Prettier: `--write` = format only
const args = formatter === 'biome' ? [...resolved.prefix, 'check', '--write', resolvedFilePath] : [...resolved.prefix, '--write', resolvedFilePath];
execFileSync(resolved.bin, args, {
cwd: projectRoot,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 15000
});
if (process.platform === 'win32' && resolved.bin.endsWith('.cmd')) {
// Windows: .cmd files require shell to execute. Guard against
// command injection by rejecting paths with shell metacharacters.
if (UNSAFE_PATH_CHARS.test(resolvedFilePath)) {
throw new Error('File path contains unsafe shell characters');
}
const result = spawnSync(resolved.bin, args, {
cwd: projectRoot,
shell: true,
stdio: 'pipe',
timeout: 15000
});
if (result.error) throw result.error;
if (typeof result.status === 'number' && result.status !== 0) {
throw new Error(result.stderr?.toString() || `Formatter exited with status ${result.status}`);
}
} else {
execFileSync(resolved.bin, args, {
cwd: projectRoot,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 15000
});
}
} catch {
// Formatter not installed, file missing, or failed — non-blocking
}