mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-15 13:23:13 +08:00
fix: port Windows hook safety fixes (#1719)
This commit is contained in:
@@ -2732,6 +2732,68 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('blocks Windows shell metacharacters before shell:true formatter execution', async () => {
|
||||
const hookPath = path.join(scriptsDir, 'post-edit-format.js');
|
||||
const resolverPath = path.join(scriptsDir, '..', 'lib', 'resolve-formatter.js');
|
||||
const childProcess = require('child_process');
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
const originalSpawnSync = childProcess.spawnSync;
|
||||
const originalExecFileSync = childProcess.execFileSync;
|
||||
const resolvedResolverPath = require.resolve(resolverPath);
|
||||
const resolvedHookPath = require.resolve(hookPath);
|
||||
const originalResolverCache = require.cache[resolvedResolverPath];
|
||||
const originalHookCache = require.cache[resolvedHookPath];
|
||||
const blockedPaths = ['semicolon;test.js', 'backtick`test.js', 'subshell$(test).js', 'group(test).js'];
|
||||
|
||||
try {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
||||
|
||||
let spawnCalls = [];
|
||||
childProcess.spawnSync = (...args) => {
|
||||
spawnCalls.push(args);
|
||||
return { status: 0, stderr: Buffer.from('') };
|
||||
};
|
||||
childProcess.execFileSync = () => {
|
||||
throw new Error('execFileSync should not run for Windows .cmd formatter shims');
|
||||
};
|
||||
|
||||
require.cache[resolvedResolverPath] = {
|
||||
id: resolvedResolverPath,
|
||||
filename: resolvedResolverPath,
|
||||
loaded: true,
|
||||
exports: {
|
||||
findProjectRoot: () => process.cwd(),
|
||||
detectFormatter: () => 'prettier',
|
||||
resolveFormatterBin: () => ({ bin: 'formatter.cmd', prefix: [] })
|
||||
}
|
||||
};
|
||||
delete require.cache[resolvedHookPath];
|
||||
|
||||
const { run } = require(hookPath);
|
||||
|
||||
for (const filePath of blockedPaths) {
|
||||
spawnCalls = [];
|
||||
const stdinJson = JSON.stringify({ tool_input: { file_path: filePath } });
|
||||
assert.strictEqual(run(stdinJson), stdinJson, 'Should pass through original stdin JSON');
|
||||
assert.strictEqual(spawnCalls.length, 0, `Should reject ${filePath} before spawnSync`);
|
||||
}
|
||||
} finally {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
}
|
||||
childProcess.spawnSync = originalSpawnSync;
|
||||
childProcess.execFileSync = originalExecFileSync;
|
||||
if (originalResolverCache) require.cache[resolvedResolverPath] = originalResolverCache;
|
||||
else delete require.cache[resolvedResolverPath];
|
||||
if (originalHookCache) require.cache[resolvedHookPath] = originalHookCache;
|
||||
else delete require.cache[resolvedHookPath];
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('matches .tsx extension for formatting', async () => {
|
||||
const stdinJson = JSON.stringify({ tool_input: { file_path: '/nonexistent/component.tsx' } });
|
||||
|
||||
@@ -952,6 +952,65 @@ async function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Windows-only: child_process.spawn cannot resolve .cmd/.bat shims for
|
||||
// bare PATH commands without an extension, and Node 18.20+/20.12+ refuse
|
||||
// to spawn .cmd targets without `shell: true` (CVE-2024-27980). The probe
|
||||
// must retry bare command names with platform extensions and route .cmd/.bat
|
||||
// through the shell, otherwise tools like `npx` are misclassified as
|
||||
// unhealthy on first use. Path-like commands keep single-candidate ENOENT
|
||||
// semantics.
|
||||
if (process.platform === 'win32') {
|
||||
if (await asyncTest('windows: probes bare PATH commands via .cmd fallback', async () => {
|
||||
const tempDir = createTempDir();
|
||||
const binDir = path.join(tempDir, 'bin');
|
||||
const configPath = path.join(tempDir, 'claude.json');
|
||||
const statePath = path.join(tempDir, 'mcp-health.json');
|
||||
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(binDir, 'winfallback.cmd'),
|
||||
['@echo off', 'node -e "setInterval(()=>{},1000)"', ''].join('\r\n')
|
||||
);
|
||||
|
||||
try {
|
||||
writeConfig(configPath, {
|
||||
mcpServers: {
|
||||
winfallback: {
|
||||
command: 'winfallback',
|
||||
args: []
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const input = { tool_name: 'mcp__winfallback__list', tool_input: {} };
|
||||
const result = runHook(input, {
|
||||
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
|
||||
ECC_MCP_CONFIG_PATH: configPath,
|
||||
ECC_MCP_HEALTH_STATE_PATH: statePath,
|
||||
ECC_MCP_HEALTH_TIMEOUT_MS: '500',
|
||||
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
result.code,
|
||||
0,
|
||||
`Expected bare command to be probed via .cmd fallback: ${hookFailureDetails(result, statePath)}`
|
||||
);
|
||||
|
||||
const state = readState(statePath);
|
||||
assert.strictEqual(
|
||||
state.servers.winfallback.status,
|
||||
'healthy',
|
||||
'Expected bare command to be marked healthy via .cmd fallback'
|
||||
);
|
||||
} finally {
|
||||
cleanupTempDir(tempDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
} else {
|
||||
console.log(' - skipped: windows: probes bare PATH commands via .cmd fallback (non-Windows)');
|
||||
}
|
||||
|
||||
if (await asyncTest('probes command servers using non-absolute commands (e.g. npx) via PATH resolution', async () => {
|
||||
const tempDir = createTempDir();
|
||||
const configPath = path.join(tempDir, 'claude.json');
|
||||
@@ -962,10 +1021,8 @@ async function runTests() {
|
||||
// Create a server script that stays alive
|
||||
fs.writeFileSync(serverScript, "setInterval(() => {}, 1000);\n");
|
||||
|
||||
// Use 'node' (non-absolute) as the command to exercise PATH-based resolution.
|
||||
// On Windows, shell execution is especially relevant for commands exposed via
|
||||
// batch wrappers such as 'npx.cmd'; using 'node' here simulates that class of
|
||||
// non-absolute command without depending on npx being available in the environment.
|
||||
// Use 'node' (non-absolute) as the command to exercise PATH-based
|
||||
// resolution without depending on npx being available in the environment.
|
||||
writeConfig(configPath, {
|
||||
mcpServers: {
|
||||
shelltest: {
|
||||
@@ -983,10 +1040,10 @@ async function runTests() {
|
||||
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 0, `Expected non-absolute command to resolve via shell, got ${result.code}`);
|
||||
assert.strictEqual(result.code, 0, `Expected non-absolute command to resolve via PATH, got ${result.code}`);
|
||||
|
||||
const state = readState(statePath);
|
||||
assert.strictEqual(state.servers.shelltest.status, 'healthy', 'Expected shell-resolved server to be marked healthy');
|
||||
assert.strictEqual(state.servers.shelltest.status, 'healthy', 'Expected PATH-resolved server to be marked healthy');
|
||||
} finally {
|
||||
cleanupTempDir(tempDir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user