mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 12:43:32 +08:00
fix: sanitize getExecCommand args, escape regex in getCommandPattern, clean up readStdinJson timeout, add 10 tests
Validate args parameter in getExecCommand() against SAFE_ARGS_REGEX to prevent command injection when returned string is passed to a shell. Escape regex metacharacters in getCommandPattern() generic action branch to prevent malformed patterns and unintended matching. Clean up stdin listeners in readStdinJson() timeout path to prevent process hanging.
This commit is contained in:
@@ -314,11 +314,15 @@ function getRunCommand(script, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allowed characters in arguments: alphanumeric, whitespace, dashes, dots, slashes,
|
||||||
|
// equals, colons, commas, quotes, @. Rejects shell metacharacters like ; | & ` $ ( ) { } < > !
|
||||||
|
const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_.\/:=,'"*+-]+$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the command to execute a package binary
|
* Get the command to execute a package binary
|
||||||
* @param {string} binary - Binary name (e.g., "prettier", "eslint")
|
* @param {string} binary - Binary name (e.g., "prettier", "eslint")
|
||||||
* @param {string} args - Arguments to pass
|
* @param {string} args - Arguments to pass
|
||||||
* @throws {Error} If binary name contains unsafe characters
|
* @throws {Error} If binary name or args contain unsafe characters
|
||||||
*/
|
*/
|
||||||
function getExecCommand(binary, args = '', options = {}) {
|
function getExecCommand(binary, args = '', options = {}) {
|
||||||
if (!binary || typeof binary !== 'string') {
|
if (!binary || typeof binary !== 'string') {
|
||||||
@@ -327,6 +331,9 @@ function getExecCommand(binary, args = '', options = {}) {
|
|||||||
if (!SAFE_NAME_REGEX.test(binary)) {
|
if (!SAFE_NAME_REGEX.test(binary)) {
|
||||||
throw new Error(`Binary name contains unsafe characters: ${binary}`);
|
throw new Error(`Binary name contains unsafe characters: ${binary}`);
|
||||||
}
|
}
|
||||||
|
if (args && typeof args === 'string' && !SAFE_ARGS_REGEX.test(args)) {
|
||||||
|
throw new Error(`Arguments contain unsafe characters: ${args}`);
|
||||||
|
}
|
||||||
|
|
||||||
const pm = getPackageManager(options);
|
const pm = getPackageManager(options);
|
||||||
return `${pm.config.execCmd} ${binary}${args ? ' ' + args : ''}`;
|
return `${pm.config.execCmd} ${binary}${args ? ' ' + args : ''}`;
|
||||||
@@ -351,6 +358,11 @@ function getSelectionPrompt() {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape regex metacharacters in a string before interpolating into a pattern
|
||||||
|
function escapeRegex(str) {
|
||||||
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a regex pattern that matches commands for all package managers
|
* Generate a regex pattern that matches commands for all package managers
|
||||||
* @param {string} action - Action pattern (e.g., "run dev", "install", "test")
|
* @param {string} action - Action pattern (e.g., "run dev", "install", "test")
|
||||||
@@ -387,12 +399,13 @@ function getCommandPattern(action) {
|
|||||||
'bun run build'
|
'bun run build'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Generic run command
|
// Generic run command — escape regex metacharacters in action
|
||||||
|
const escaped = escapeRegex(action);
|
||||||
patterns.push(
|
patterns.push(
|
||||||
`npm run ${action}`,
|
`npm run ${escaped}`,
|
||||||
`pnpm( run)? ${action}`,
|
`pnpm( run)? ${escaped}`,
|
||||||
`yarn ${action}`,
|
`yarn ${escaped}`,
|
||||||
`bun run ${action}`
|
`bun run ${escaped}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,11 @@ async function readStdinJson(options = {}) {
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
settled = true;
|
settled = true;
|
||||||
|
// Clean up stdin listeners so the event loop can exit
|
||||||
|
process.stdin.removeAllListeners('data');
|
||||||
|
process.stdin.removeAllListeners('end');
|
||||||
|
process.stdin.removeAllListeners('error');
|
||||||
|
if (process.stdin.unref) process.stdin.unref();
|
||||||
// Resolve with whatever we have so far rather than hanging
|
// Resolve with whatever we have so far rather than hanging
|
||||||
try {
|
try {
|
||||||
resolve(data.trim() ? JSON.parse(data) : {});
|
resolve(data.trim() ? JSON.parse(data) : {});
|
||||||
|
|||||||
@@ -874,6 +874,62 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// ─── Round 21: getExecCommand args validation ───
|
||||||
|
console.log('\ngetExecCommand (args validation):');
|
||||||
|
|
||||||
|
if (test('rejects args with shell metacharacter semicolon', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand('prettier', '; rm -rf /'), /unsafe characters/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects args with pipe character', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand('prettier', '--write . | cat'), /unsafe characters/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects args with backtick injection', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand('prettier', '`whoami`'), /unsafe characters/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects args with dollar sign', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand('prettier', '$HOME'), /unsafe characters/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects args with ampersand', () => {
|
||||||
|
assert.throws(() => pm.getExecCommand('prettier', '--write . && echo pwned'), /unsafe characters/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows safe args like --write .', () => {
|
||||||
|
const cmd = pm.getExecCommand('prettier', '--write .');
|
||||||
|
assert.ok(cmd.includes('--write .'), 'Should include safe args');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('allows empty args without trailing space', () => {
|
||||||
|
const cmd = pm.getExecCommand('prettier', '');
|
||||||
|
assert.ok(!cmd.endsWith(' '), 'Should not have trailing space for empty args');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// ─── Round 21: getCommandPattern regex escaping ───
|
||||||
|
console.log('\ngetCommandPattern (regex escaping):');
|
||||||
|
|
||||||
|
if (test('escapes dot in action name for regex safety', () => {
|
||||||
|
const pattern = pm.getCommandPattern('test.all');
|
||||||
|
// The dot should be escaped to \\. in the pattern
|
||||||
|
const regex = new RegExp(pattern);
|
||||||
|
assert.ok(regex.test('npm run test.all'), 'Should match literal dot');
|
||||||
|
assert.ok(!regex.test('npm run testXall'), 'Should NOT match arbitrary character in place of dot');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('escapes brackets in action name', () => {
|
||||||
|
const pattern = pm.getCommandPattern('build[prod]');
|
||||||
|
const regex = new RegExp(pattern);
|
||||||
|
assert.ok(regex.test('npm run build[prod]'), 'Should match literal brackets');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('escapes parentheses in action name', () => {
|
||||||
|
// Should not throw when compiled as regex
|
||||||
|
const pattern = pm.getCommandPattern('foo(bar)');
|
||||||
|
assert.doesNotThrow(() => new RegExp(pattern), 'Should produce valid regex with escaped parens');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
console.log('\n=== Test Results ===');
|
console.log('\n=== Test Results ===');
|
||||||
console.log(`Passed: ${passed}`);
|
console.log(`Passed: ${passed}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user