fix: expand deny rules and harden hook regex for rm and pipe-to-shell bypasses

Deny rules previously only blocked `rm -rf` and `rm -fr` (lowercase, combined
flags only). All other flag forms bypassed both security layers silently.

Expand deny rules to cover:
- All case combinations of combined short flags: -Rf, -rF, -RF, -fR, -Fr, -FR
- All orderings of separated short flags: -r -f, -r -F, -R -f, -R -F and
  their reverses (-f -r, -f -R, -F -r, -F -R)
- GNU long-form flags: --recursive, --recursive --force, --force --recursive
- Pipe-to-shell via sh and zsh: * | sh, * | zsh
- Process substitution: bash <(curl *)
- Spaced pipe variant for existing rules: curl *| bash*, wget *| bash*

Harden PreToolUse hook regex:
- Old regex required combined lowercase flags only (-rf / -fr pattern)
- New regex uses three chained case-insensitive grep checks:
  1. Confirm rm is the actual command (not a substring of another word or arg)
  2. Detect any recursive flag (-r, -R, -[flags]r, --recursive)
  3. Detect any force flag (-f, -F, -[flags]f, --force)
- Handles commands chained with ; && || |
- Verified against 25 test cases (19 must-block, 6 must-allow)
This commit is contained in:
newklei
2026-02-23 11:28:44 -05:00
parent df8374dd00
commit 58b6f73e2a
+24 -2
View File
@@ -13,11 +13,33 @@
"deny": [
"Bash(rm -rf *)",
"Bash(rm -fr *)",
"Bash(rm -Rf *)",
"Bash(rm -rF *)",
"Bash(rm -RF *)",
"Bash(rm -fR *)",
"Bash(rm -Fr *)",
"Bash(rm -FR *)",
"Bash(rm -r -f *)",
"Bash(rm -r -F *)",
"Bash(rm -R -f *)",
"Bash(rm -R -F *)",
"Bash(rm -f -r *)",
"Bash(rm -f -R *)",
"Bash(rm -F -r *)",
"Bash(rm -F -R *)",
"Bash(rm --recursive *)",
"Bash(rm --recursive --force *)",
"Bash(rm --force --recursive *)",
"Bash(sudo *)",
"Bash(mkfs *)",
"Bash(dd *)",
"Bash(curl *|bash*)",
"Bash(curl *| bash*)",
"Bash(wget *|bash*)",
"Bash(wget *| bash*)",
"Bash(* | sh)",
"Bash(* | zsh)",
"Bash(bash <(curl *))",
"Bash(git push --force*)",
"Bash(git push *--force*)",
"Bash(git reset --hard*)",
@@ -53,11 +75,11 @@
"hooks": [
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f'; then echo 'BLOCKED: Use trash instead of rm -rf' >&2; exit 2; fi"
"command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qiE '(^|;[[:space:]]*|&&[[:space:]]*|[|][|][[:space:]]*|[|][[:space:]]*)rm[[:space:]]' && echo \"$CMD\" | grep -qiE '(^|[[:space:]])-[a-zA-Z]*[rR]|--recursive' && echo \"$CMD\" | grep -qiE '(^|[[:space:]])-[a-zA-Z]*[fF]|--force'; then echo 'BLOCKED: Use trash instead of rm -rf' >&2; exit 2; fi"
},
{
"type": "command",
"command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'git[[:space:]]+push.*(main|master)'; then echo 'BLOCKED: Use feature branches, not direct push to main' >&2; exit 2; fi"
"command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qiE 'git[[:space:]]+push.*(main|master)'; then echo 'BLOCKED: Use feature branches, not direct push to main' >&2; exit 2; fi"
}
]
}