ktg-plugin-marketplace/plugins/llm-security/examples/bash-evasion-gallery
Kjell Tore Guttormsen ca5a8cec67 feat(llm-security): add 3 more runnable threat examples [skip-docs]
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).
2026-05-05 15:01:20 +02:00
..
expected-findings.md feat(llm-security): add 3 more runnable threat examples [skip-docs] 2026-05-05 15:01:20 +02:00
README.md feat(llm-security): add 3 more runnable threat examples [skip-docs] 2026-05-05 15:01:20 +02:00
run-evasion-gallery.mjs feat(llm-security): add 3 more runnable threat examples [skip-docs] 2026-05-05 15:01:20 +02:00

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

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