Three new self-contained, runnable threat demonstrations under
examples/, continuing the batch started in 583a78c. Each example
has README.md + run-*.mjs + expected-findings.md and uses
state-isolation discipline so the user's real cache/state files
are never polluted.
- examples/supply-chain-attack/ — two-layer demonstration:
pre-install-supply-chain (PreToolUse) blocks compromised
event-stream version 3.3.6 and emits a scope-hop advisory for
the @evilcorp scope; dep-auditor (DEP scanner, offline) flags
5 typosquat dependencies plus a curl-piped install-script
vector in the fixture package.json. Maps to LLM03/LLM05/ASI04.
- examples/poisoned-claude-md/ — all 6 memory-poisoning detectors
fire on a deliberately poisoned CLAUDE.md plus a fixture
agent file under .claude/agents (E15/v7.2.0 surface):
detectInjection, detectShellCommands, detectSuspiciousUrls,
detectCredentialPaths, detectPermissionExpansion,
detectEncodedPayloads. No agent runtime needed — scanner
imported directly. Maps to LLM01/LLM06/ASI04.
- examples/bash-evasion-gallery/ — one disguised variant per
T1 through T9 evasion technique fed through pre-bash-destructive,
verified BLOCK after bash-normalize strips the evasion. T8
base64-pipe-shell uses its own BLOCK_RULE. The canonical
destructive form uses a path token rather than the bare slash
(regex word-boundary requires it). Source-string fragmentation
pattern reused from the e2e attack-chain test. Maps to
LLM06/ASI01/LLM01.
Plugin README "Other runnable examples" section + plugin
CLAUDE.md "Examples" table + CHANGELOG Unreleased/Added
all updated. Marketplace root README unchanged
([skip-docs] for marketplace-level gate — plugin's outward
coverage is unchanged, only demonstrations were added).
131 lines
5.8 KiB
Markdown
131 lines
5.8 KiB
Markdown
# 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`,
|
|
`/<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
|
|
|
|
```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
|