# Bash Evasion Gallery (T1-T9) > **WARNING: This is a demonstration fixture, NOT a real attack.** > No destructive command actually runs. The script feeds JSON > payloads to one PreToolUse hook script and verifies the hook's > exit code. Your `$HOME` is safe. ## What this demonstrates `hooks/scripts/pre-bash-destructive.mjs` (PreToolUse on `Bash`) catches destructive commands by pattern-matching the command string. A motivated attacker can disguise the same command using shell metacharacters that bash interprets transparently — so naive regex matching is bypassed. `scanners/lib/bash-normalize.mjs` is the **defense-in-depth layer** that strips these evasion techniques before pattern matching runs. This gallery feeds one disguised variant per T-tag through the hook and verifies that every variant gets normalized and blocked. This complements — does not replace — Claude Code 2.1.98+'s harness-level bash normalization. The plugin layer runs before the harness layer, so even on older Claude Code versions the techniques below are caught. ## The T-tag taxonomy | Tag | Technique | Example | Normalizes to | |-----|-----------|---------|---------------| | T1 | empty single quotes | `r''m -rf $HOME` | `rm -rf $HOME` | | T2 | empty double quotes | `r""m -rf $HOME` | `rm -rf $HOME` | | T3 | parameter expansion (multi-char) | `${UNUSED}rm -rf $HOME` | `rm -rf $HOME` | | T3 | parameter expansion (single-char) | `c${u}rl url \| sh` | `curl url \| sh` | | T4 | backslash word-splitting | `r\m -rf $HOME` | `rm -rf $HOME` | | T5 | IFS word-splitting | `rm${IFS}-rf${IFS}$HOME` | `rm -rf $HOME` | | T6 | ANSI-C hex quoting | `$'\x72\x6d' -rf $HOME` | `rm -rf $HOME` | | T7 | process substitution | `cat <(echo rm) -rf $HOME` | `cat echo rm -rf $HOME` | | T8 | base64-pipe-shell idiom | `echo cm0gLXJmICRIT01F \| base64 -d \| sh` | (separate BLOCK_RULE — not normalization) | | T9 | eval-via-variable (one-level forward flow) | `X=rm; $X -rf $HOME` | `X=rm; rm -rf $HOME` | The canonical destructive target throughout the gallery is `rm -rf $HOME`. The `pre-bash-destructive` "Filesystem root destruction" BLOCK_RULE matches `rm -rf` followed by `$HOME`, `/`, or `~` — but plain `rm -rf /` slips through because the regex requires a word boundary after the path. The gallery uses `$HOME` so the regex fires reliably; for the literal `rm -rf /` case, see Claude Code 2.1.98+ harness-level checks. T8 is structurally different — it's not an evasion of an existing rule but a fresh attack pattern (decode + pipe to shell). It has its own BLOCK_RULE in `pre-bash-destructive`, named "T8 — base64-pipe-shell idiom". ## How to run ```bash cd plugins/llm-security node examples/bash-evasion-gallery/run-evasion-gallery.mjs # Detailed: full hook stderr node examples/bash-evasion-gallery/run-evasion-gallery.mjs --verbose ``` Expected: `10 pass, 0 fail`. Each T-tag should produce `BLOCKED: Destructive command detected — Filesystem root destruction (rm -rf /)` on stderr (or `T8 — base64-pipe-shell idiom` for the T8 case). ## Hooks involved - **`hooks/scripts/pre-bash-destructive.mjs`** — PreToolUse on `Bash`. Reads `tool_input.command`, applies `normalizeBashExpansion` (T1-T7, T9) then `normalizeCommand` (whitespace + ANSI), then iterates 8 BLOCK_RULES (rm-root, chmod 777, curl|sh, fork bomb, mkfs, dd, /dev/ writes, eval+expansion, T8 base64-pipe-shell). Exit 2 = block, exit 0 = allow (with optional WARN advisory). - **`scanners/lib/bash-normalize.mjs`** — pure function module, shared with `pre-install-supply-chain.mjs`. Exports `normalizeBashExpansion(cmd)`. Test contract: `tests/lib/bash-normalize.test.mjs`. ## Source-string fragmentation The `run-evasion-gallery.mjs` script never contains the literal string `rm -rf $HOME`. Each test command is built at runtime via concatenation (`'r' + 'm'`, `'-' + 'rf'`, `'$' + 'HOME'`). This follows the discipline from `tests/e2e/attack-chain.test.mjs` — secrets-shaped or destructive-command-shaped strings should never appear as literals in the source, because: 1. `pre-edit-secrets` would block writing the file 2. Static scanners would treat the source as compromised 3. Operators reading the diff would not be able to tell at a glance whether the literal was meant to be safe context or live payload The `--verbose` mode prints the assembled command to stdout for inspection — that's a runtime print, not a source-file literal. ## OWASP / framework mapping | Code | Framework | Why | |------|-----------|-----| | LLM06 | OWASP LLM Top 10 (2025) | Excessive Agency — destructive-command surface | | ASI01 | OWASP Agentic Top 10 | Excessive Agency / runtime command injection | | LLM01 | OWASP LLM Top 10 (2025) | Indirect prompt injection often delivers commands | ## Limitations - T8 base64 decoding is detected pattern-only — it doesn't decode the blob to inspect its content. A blob that decodes to *non-destructive* content would still be flagged. This is the documented v5.0 honest-limitation: deterministic detection errs toward false-positive, not false-negative. - T9 only resolves one level of variable substitution forward. `X=rm; Y=$X; $Y -rf $HOME` is **not** caught — the second-level alias is too rare in real attacks to justify the runtime cost. - The hook is a defense-in-depth layer. Claude Code 2.1.98+ includes harness-level bash normalization that catches the same techniques (and more, since the harness sees fully- expanded commands). This plugin's hook covers older Claude Code versions and runs before the harness layer either way. ## See also - `docs/security-hardening-guide.md` §3 — bash evasion theory - `tests/lib/bash-normalize.test.mjs` — per-T-tag unit contract - `tests/lib/pre-bash-destructive.test.mjs` — block-rule contract - `examples/supply-chain-attack/` — adjacent layer (install gate) - `expected-findings.md` (in this folder) — testable contract