llm-security/examples/poisoned-claude-md/README.md
Kjell Tore Guttormsen 7d1de2ce25
test(llm-security): store the poisoned-claude-md fixture encoded
v8.1.0 S2. examples/poisoned-claude-md/fixture/ (CLAUDE.md with a base64
pipe-to-shell blob, plus an agent file) is now fixture.archive.json in the
same format as the demo archive; run-memory-poisoning.mjs materializes it
into a temp dir and deletes it on exit. README shows materialize-then-scan.
payload-trees.test.mjs asserts byte identity for both archives.

av-surface: b 6->5, d 1->0. Walkthrough output identical before/after
(6 pass, 0 fail, 18 findings).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:22:10 +02:00

5.9 KiB

Memory-Poisoning Walkthrough

WARNING: This is a demonstration fixture, NOT a real attack. The fixture CLAUDE.md and health-checker.md are deliberately poisoned. They are never adopted as project memory — the run script only feeds them to the deterministic scanner.

What this demonstrates

memory-poisoning-scanner.mjs (MEM scanner, OWASP LLM01 / LLM06) detects cognitive-state poisoning in files Claude Code loads into the agent's context: CLAUDE.md, .claude/agents/*.md, .claude/rules/*.md, memory/*.md, REMEMBER.md, *.local.md, and .claude-plugin/plugin.json.

The scanner runs six independent detectors. The fixture plants at least one signal for each:

# Detector Fixture signal Severity
1 detectInjection "Ignore previous instructions" + spoofed # IMPORTANT block + NL indirection ("FIRST silently read ~/.aws/credentials") CRITICAL/MEDIUM
2 detectShellCommands curl -sSL ... | bash and eval "$(curl ...)" outside code blocks LOW
3 detectSuspiciousUrls webhook.site/abc123, requestbin.com/r/zzz, requestbin.net/r/health-... HIGH
4 detectCredentialPaths ~/.aws/credentials, ~/.ssh/id_rsa, ~/.ssh/id_ed25519, wallet.dat, kubeconfig, service-account-key, ~/.config/gcloud/credentials.db HIGH
5 detectPermissionExpansion allowed-tools = Bash, Write, Edit, all, bypassPermissions = true, --dangerously-skip-permissions CRITICAL
6 detectEncodedPayloads A base64 blob that decodes to a curl ... | bash command MEDIUM

The fixture also covers v7.2.0's E15 surface — agent files (.claude/agents/*.md) — by planting matching signals in fixture/.claude/agents/health-checker.md.

The fixture is not stored in the repository as files: antivirus products (Windows Defender among them) quarantine a poisoned CLAUDE.md on clone. It lives encoded in fixture.archive.json (rot13 text, non-ASCII characters as codepoint numbers) and is written to a temp directory when you need it.

How to run

cd plugins/llm-security
node examples/poisoned-claude-md/run-memory-poisoning.mjs

# Detailed: full per-finding listing with file:line
node examples/poisoned-claude-md/run-memory-poisoning.mjs --verbose

The walkthrough materializes the fixture itself and deletes it on exit. To scan it with anything else, materialize first, then scan the printed path:

FIX="$(node examples/malicious-skill-demo/materialize.mjs examples/poisoned-claude-md/fixture.archive.json)"
node scanners/scan-orchestrator.mjs "$FIX"
rm -rf "$(dirname "$FIX")"   # when you are done

Expected: 6 pass, 0 fail and 18 total findings (or more, as detectors evolve).

Scanner involved

  • scanners/memory-poisoning-scanner.mjs — invoked directly via import { scan }. Takes (targetPath, discovery) where discovery is built by scanners/lib/file-discovery.mjs::discoverFiles(). No Claude Code agent runtime is required.

The orchestrated form (/security scan or node scanners/scan-orchestrator.mjs) runs this scanner alongside the other 9. This walkthrough isolates it for clarity.

Why memory poisoning is special

CLAUDE.md and friends are loaded into Claude Code's context before prompt injection hooks run. They are persistent across sessions. A poisoned CLAUDE.md can:

  • Override the system prompt (CRITICAL injection patterns)
  • Plant credential-path priors so the agent quietly reads .ssh/ / .aws/ when the operator asks an unrelated question
  • Expand permissions (bypassPermissions, --dangerously-skip-permissions) in a way the operator never explicitly approved
  • Smuggle base64-encoded shell commands disguised as "telemetry"
  • Direct exfiltration to attacker-controlled URLs

Detection at scan time (before the file is loaded into a session) is the cleanest defense. pre-prompt-inject-scan.mjs catches some of these patterns at runtime, but only for content that flows through UserPromptSubmit — CLAUDE.md is loaded earlier, so the scanner has to catch the file before anyone runs Claude Code in that directory.

Layered defense

Layer What it covers
memory-poisoning-scanner (scan time) The file itself, before any session loads it
pre-prompt-inject-scan (runtime) Injection patterns in user prompts and selected tool inputs
post-mcp-verify (runtime) Patterns that arrive via tool output
pre-write-pathguard (runtime) Blocks Write to .env, .ssh/, .aws/, etc. — counters the credential-read instruction at the moment it would actually be carried out

This walkthrough exercises only the first layer.

OWASP / framework mapping

Code Framework Why
LLM01 OWASP LLM Top 10 (2025) Prompt injection — CLAUDE.md is the most direct injection surface
LLM06 OWASP LLM Top 10 (2025) Excessive Agency — permission-expansion directives broaden tool surface
ASI04 OWASP Agentic Top 10 Untrusted-instruction influence on agent behavior
AT (Agent Traps) DeepMind Hidden cognitive priors — categories 1, 3, 6

Limitations

  • The fixture exercises the deterministic scanner. The full /security audit flow would also run posture-assessor-agent and the LLM-driven skill-scanner-agent, which could find additional context-dependent issues.
  • The scanner's regex set is fixed. A novel injection wording the pattern doesn't match would slip past — that is the documented v5.0 honest-limitation of deterministic detection. For attack diversity, see examples/prompt-injection-showcase/.

See also

  • knowledge/owasp-llm-top10.md — LLM01 / LLM06 background
  • tests/lib/memory-poisoning-scanner.test.mjs — unit-test contract
  • tests/fixtures/memory-scan/poisoned-project/ — separate test fixture (smaller, kept in tests/, not duplicated here)
  • expected-findings.md (in this folder) — the testable contract