From 58b6f73e2a5cb244e48a42eb5811cefe14a107d6 Mon Sep 17 00:00:00 2001 From: newklei Date: Mon, 23 Feb 2026 11:28:44 -0500 Subject: [PATCH] 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) --- settings.json | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/settings.json b/settings.json index 3956051..9b11d06 100644 --- a/settings.json +++ b/settings.json @@ -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" } ] }