mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 11:23:32 +08:00
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:
@@ -17,9 +17,12 @@
|
|||||||
* Fails silently if no formatter is found or installed.
|
* Fails silently if no formatter is found or installed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execFileSync } = require('child_process');
|
const { execFileSync, spawnSync } = require('child_process');
|
||||||
const path = require('path');
|
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 { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
|
||||||
|
|
||||||
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
||||||
@@ -50,11 +53,29 @@ function run(rawInput) {
|
|||||||
// Prettier: `--write` = format only
|
// Prettier: `--write` = format only
|
||||||
const args = formatter === 'biome' ? [...resolved.prefix, 'check', '--write', resolvedFilePath] : [...resolved.prefix, '--write', resolvedFilePath];
|
const args = formatter === 'biome' ? [...resolved.prefix, 'check', '--write', resolvedFilePath] : [...resolved.prefix, '--write', resolvedFilePath];
|
||||||
|
|
||||||
execFileSync(resolved.bin, args, {
|
if (process.platform === 'win32' && resolved.bin.endsWith('.cmd')) {
|
||||||
cwd: projectRoot,
|
// Windows: .cmd files require shell to execute. Guard against
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
// command injection by rejecting paths with shell metacharacters.
|
||||||
timeout: 15000
|
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 {
|
} catch {
|
||||||
// Formatter not installed, file missing, or failed — non-blocking
|
// Formatter not installed, file missing, or failed — non-blocking
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function main() {
|
|||||||
if (hookModule && typeof hookModule.run === 'function') {
|
if (hookModule && typeof hookModule.run === 'function') {
|
||||||
try {
|
try {
|
||||||
const output = hookModule.run(raw);
|
const output = hookModule.run(raw);
|
||||||
if (output != null) process.stdout.write(output);
|
if (output !== null && output !== undefined) process.stdout.write(output);
|
||||||
} catch (runErr) {
|
} catch (runErr) {
|
||||||
process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`);
|
process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`);
|
||||||
process.stdout.write(raw);
|
process.stdout.write(raw);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function detectFormatter(projectRoot) {
|
|||||||
const pkgPath = path.join(projectRoot, 'package.json');
|
const pkgPath = path.join(projectRoot, 'package.json');
|
||||||
if (fs.existsSync(pkgPath)) {
|
if (fs.existsSync(pkgPath)) {
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
if (pkg.prettier != null) {
|
if ('prettier' in pkg) {
|
||||||
formatterCache.set(projectRoot, 'prettier');
|
formatterCache.set(projectRoot, 'prettier');
|
||||||
return 'prettier';
|
return 'prettier';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1966,8 +1966,10 @@ async function runTests() {
|
|||||||
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
|
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
|
||||||
// Strip comments to avoid matching "shell: true" in comment text
|
// Strip comments to avoid matching "shell: true" in comment text
|
||||||
const codeOnly = formatSource.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
const codeOnly = formatSource.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
assert.ok(!codeOnly.includes('shell:'), 'post-edit-format.js should not pass shell option in code');
|
assert.ok(!/execFileSync\([^)]*shell\s*:/.test(codeOnly), 'post-edit-format.js should not pass shell option to execFileSync');
|
||||||
// npx.cmd handling moved to shared resolve-formatter.js — verify it uses the shared module
|
assert.ok(codeOnly.includes("process.platform === 'win32' && resolved.bin.endsWith('.cmd')"), 'Windows shell execution must stay gated to .cmd shims');
|
||||||
|
assert.ok(codeOnly.includes('UNSAFE_PATH_CHARS'), 'Must guard against shell metacharacters before using shell: true');
|
||||||
|
// npx.cmd handling in shared resolve-formatter.js
|
||||||
const resolverSource = fs.readFileSync(path.join(scriptsDir, '..', 'lib', 'resolve-formatter.js'), 'utf8');
|
const resolverSource = fs.readFileSync(path.join(scriptsDir, '..', 'lib', 'resolve-formatter.js'), 'utf8');
|
||||||
assert.ok(resolverSource.includes('npx.cmd'), 'resolve-formatter.js should use npx.cmd for Windows cross-platform safety');
|
assert.ok(resolverSource.includes('npx.cmd'), 'resolve-formatter.js should use npx.cmd for Windows cross-platform safety');
|
||||||
assert.ok(formatSource.includes('resolveFormatterBin'), 'post-edit-format.js should use shared resolveFormatterBin');
|
assert.ok(formatSource.includes('resolveFormatterBin'), 'post-edit-format.js should use shared resolveFormatterBin');
|
||||||
|
|||||||
Reference in New Issue
Block a user