mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-19 11:20:48 +08:00
fix: detect destructive find -exec commands in gateguard (#2267)
* fix: detect destructive find exec commands in gateguard * chore: ignore aider local files
This commit is contained in:
parent
68e926bf77
commit
a6ac0273e2
2
.gitignore
vendored
2
.gitignore
vendored
@ -100,3 +100,5 @@ ecc2/target/
|
||||
.opencode/package-lock.json
|
||||
.opencode/node_modules/
|
||||
assets/images/security/badrudi-exploit.mp4
|
||||
|
||||
.aider*
|
||||
|
||||
@ -461,6 +461,75 @@ function collectExecutableBodies(raw) {
|
||||
return bodies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect destructive commands inside `find ... -exec` invocations.
|
||||
* Handles `-exec rm {} \;`, `-exec rm -rf {} \;`, `-exec rmdir {} \;`,
|
||||
* `-exec unlink {} \;`, `-exec git reset --hard {} \;`.
|
||||
*
|
||||
* @param {string} command
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDestructiveFindExec(command) {
|
||||
const raw = String(command || '');
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tokenize the whole command line
|
||||
const tokens = tokenize(trimmed);
|
||||
if (!tokens || tokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must start with `find`
|
||||
if (commandBasename(tokens[0]) !== 'find') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the `-exec` token
|
||||
const execIndex = tokens.indexOf('-exec');
|
||||
if (execIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect tokens after `-exec` until we hit a terminator (`;`, `\;`, or `+`)
|
||||
const execTokens = [];
|
||||
for (let i = execIndex + 1; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (token === ';' || token === '\\;' || token === '+') {
|
||||
break;
|
||||
}
|
||||
execTokens.push(token);
|
||||
}
|
||||
|
||||
if (execTokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseCmd = commandBasename(execTokens[0]);
|
||||
|
||||
// Directly destructive commands inside -exec
|
||||
if (baseCmd === 'rmdir' || baseCmd === 'unlink') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `rm` with any flags (including none) inside -exec is destructive
|
||||
if (baseCmd === 'rm') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `git reset --hard` inside -exec
|
||||
if (baseCmd === 'git') {
|
||||
const sub = findGitSubcommand(execTokens);
|
||||
if (sub && sub.command === 'reset' && sub.rest.includes('--hard')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDestructiveBash(command) {
|
||||
// The SQL/dd phrases live in command bodies, not as flag-bearing
|
||||
// arguments, so we still match them by regex — but on the input
|
||||
@ -476,6 +545,9 @@ function isDestructiveBash(command) {
|
||||
const extra = getExtraDestructiveRegex();
|
||||
if (extra && extra.test(flattened)) return true;
|
||||
|
||||
// Check for destructive find -exec patterns
|
||||
if (isDestructiveFindExec(raw)) return true;
|
||||
|
||||
const segments = collectExecutableBodies(raw).flatMap(splitCommandSegments);
|
||||
for (const segment of segments) {
|
||||
const stripped = stripQuotedStrings(segment);
|
||||
|
||||
@ -1586,6 +1586,49 @@ function runTests() {
|
||||
'custom phrase inside command substitution should be gated');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- find -exec destructive detection ---
|
||||
|
||||
if (test('denies find -exec rm {} \\; as destructive', () => {
|
||||
expectDestructiveDeny('find . -name "*.tmp" -exec rm {} \\;',
|
||||
'find -exec rm');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies find -exec rm -rf {} \\; as destructive', () => {
|
||||
expectDestructiveDeny('find . -name "*.tmp" -exec rm -rf {} \\;',
|
||||
'find -exec rm -rf');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies find -exec rmdir {} \\; as destructive', () => {
|
||||
expectDestructiveDeny('find . -name "*.tmp" -exec rmdir {} \\;',
|
||||
'find -exec rmdir');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies find -exec unlink {} \\; as destructive', () => {
|
||||
expectDestructiveDeny('find . -name "*.tmp" -exec unlink {} \\;',
|
||||
'find -exec unlink');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies find -exec git reset --hard {} \\; as destructive', () => {
|
||||
expectDestructiveDeny('find . -name "*.tmp" -exec git reset --hard {} \\;',
|
||||
'find -exec git reset --hard');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows find -exec echo {} \\; (non-destructive, routine gate)', () => {
|
||||
clearState();
|
||||
const input = { tool_name: 'Bash', tool_input: { command: 'find . -name "*.tmp" -exec echo {} \\;' } };
|
||||
const result = runBashHook(input);
|
||||
assert.strictEqual(result.code, 0, 'exit code should be 0');
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce JSON output');
|
||||
// Should be denied by routine gate (first bash), not destructive gate
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'should be denied by routine gate');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('Destructive'),
|
||||
'should not be the destructive deny message');
|
||||
assert.ok(!output.hookSpecificOutput.permissionDecisionReason.includes('rollback'),
|
||||
'should not mention rollback');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Issue #2078 review fix: warning emitted once per *distinct*
|
||||
// invalid regex, not once per process. Verifies the same-process
|
||||
// path that the reviewers (CodeRabbit + cubic) flagged.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user