Three new self-contained, runnable threat demonstrations under
examples/, continuing the batch started in
|
||
|---|---|---|
| .. | ||
| expected-findings.md | ||
| README.md | ||
| run-evasion-gallery.mjs | ||
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
$HOMEis 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,
/<path>, or ~<path> — 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
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 onBash. Readstool_input.command, appliesnormalizeBashExpansion(T1-T7, T9) thennormalizeCommand(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 withpre-install-supply-chain.mjs. ExportsnormalizeBashExpansion(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:
pre-edit-secretswould block writing the file- Static scanners would treat the source as compromised
- 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 $HOMEis 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 theorytests/lib/bash-normalize.test.mjs— per-T-tag unit contracttests/lib/pre-bash-destructive.test.mjs— block-rule contractexamples/supply-chain-attack/— adjacent layer (install gate)expected-findings.md(in this folder) — testable contract