fix(careful): warn on chained rm even when the last target is safe

The safe-exception block whitelisted rm -rf of build artifacts by
extracting targets with a single greedy match (.*rm ...), which only ever
inspects the LAST rm in the command. A chain like 'rm -rf /; rm -rf
node_modules' was therefore judged solely by its trailing safe target and
allowed without warning, waving through the destructive 'rm -rf /'.

Gate the shortcut to single rm invocations: when any shell separator
(; | & newline, incl. JSON-escaped \n/\r from the grep extraction path)
is present, fall through to the destructive-pattern check, which warns on
any recursive rm. Single-command artifact cleanups still allow.

Adds 3 regression tests covering semicolon and && chains in both orders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jayesh Betala 2026-06-18 14:48:12 +05:30 committed by Garry Tan
parent 11de390be1
commit b22f7a66e1
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 38 additions and 1 deletions

View File

@ -26,7 +26,19 @@ fi
CMD_LOWER=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]')
# --- Check for safe exceptions (rm -rf of build artifacts) ---
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then
# Only whitelist when the command is a SINGLE rm invocation. The target
# extraction below greedily matches the LAST `rm ` in the string, so a chained
# command like `rm -rf /; rm -rf node_modules` would be judged solely by its
# final (safe) targets and wave through the destructive earlier `rm -rf /`.
# When any shell separator is present, skip the shortcut and fall through to the
# destructive-pattern check, which warns on any recursive rm.
HAS_SEPARATOR=false
case "$CMD" in
# Real separators, plus JSON-escaped newlines (\n / \r) which survive the
# grep extraction path as literal two-char sequences and still mark a chain.
*';'*|*'|'*|*'&'*|*$'\n'*|*$'\r'*|*'\n'*|*'\r'*) HAS_SEPARATOR=true ;;
esac
if [ "$HAS_SEPARATOR" = false ] && printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then
SAFE_ONLY=true
RM_ARGS=$(printf '%s' "$CMD" | sed -E 's/.*rm[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*//;s/--recursive[[:space:]]*//')
for target in $RM_ARGS; do

View File

@ -96,6 +96,31 @@ describe('check-careful.sh', () => {
expect(output.permissionDecision).toBe('ask');
expect(output.message).toContain('recursive delete');
});
// Regression: the safe-exception extracts targets from only the LAST `rm` in
// the command (greedy match), so a chain that ends in a safe target must not
// wave through a destructive earlier rm. The shortcut only applies to a
// single rm invocation; any shell separator falls through to the warning.
test('rm -rf /; rm -rf node_modules warns (semicolon chain, dangerous first)', () => {
const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf /; rm -rf node_modules'));
expect(exitCode).toBe(0);
expect(output.permissionDecision).toBe('ask');
expect(output.message).toContain('recursive delete');
});
test('rm -rf /etc/data && rm -rf dist warns (&& chain, dangerous first)', () => {
const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf /etc/data && rm -rf dist'));
expect(exitCode).toBe(0);
expect(output.permissionDecision).toBe('ask');
expect(output.message).toContain('recursive delete');
});
test('rm -rf node_modules; rm -rf /home/user/data warns (safe first, dangerous last)', () => {
const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf node_modules; rm -rf /home/user/data'));
expect(exitCode).toBe(0);
expect(output.permissionDecision).toBe('ask');
expect(output.message).toContain('recursive delete');
});
});
// --- SQL destructive commands ---