harden(careful): substitution separators + capital -R recursive flag (#2039)

Two residual fail-opens in the same guard PR #2040 hardened, both verified
by executing the script pre-fix:

- rm -rf $(./wipe-all)/node_modules silently allowed: the substitution token
  ends in a whitelisted suffix and the safe-exception early exit skipped ALL
  downstream checks. $( and backtick now count as chain separators; plain
  $VAR expansion stays allowed.
- rm -R / silently allowed: both greps required a lowercase r in the flag
  cluster; capital -R is the documented BSD/macOS recursive flag. Both greps
  now match -[a-zA-Z]*[rR].

Six new tests: substitution x2 -> ask, capital-R x2 -> ask, rm -Rf
node_modules single-command -> still allowed, escaped-newline branch
(existing code, previously untested), and a pinned deliberate FP
(cd app && rm -rf node_modules -> ask) documenting the fail-closed
direction on chains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-09 18:57:56 -07:00
co-authored by Claude Fable 5
parent b22f7a66e1
commit 5d23ccab56
2 changed files with 68 additions and 4 deletions
+8 -4
View File
@@ -36,9 +36,13 @@ 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 ;;
# Command/backtick substitution counts as chaining too: a token like
# `$(./wipe-all)/node_modules` ends in a whitelisted suffix while running
# anything inside the substitution. Plain $VAR expansion stays allowed —
# only `$(` triggers.
*';'*|*'|'*|*'&'*|*'$('*|*'`'*|*$'\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
if [ "$HAS_SEPARATOR" = false ] && printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*[rR][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
@@ -63,8 +67,8 @@ fi
WARN=""
PATTERN=""
# rm -rf / rm -r / rm --recursive
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r|--recursive)' 2>/dev/null; then
# rm -rf / rm -r / rm -R / rm --recursive (capital -R is BSD/macOS recursive)
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*[rR]|--recursive)' 2>/dev/null; then
WARN="Destructive: recursive delete (rm -r). This permanently removes files."
PATTERN="rm_recursive"
fi