2026-05-20-waf-cmdi-backtick-narrow

2026-05-20 — WAF cmdi_backtick narrowed + response-phase fence-strip (Phase 3 of OpenClaw stress fixes)

Summary

OpenClaw quality stress 2026-05-20 finding: the response-phase WAF was firing cmdi_backtick on every fenced JSON markdown block in legitimate model output (e.g. `

\n{"status":"ok"}\n

), blocking valid completions. The original regex matched any text between two backticks — a substring as benign as true ` tripped it.

This change does two things:

  1. Narrow cmdi_backtick to require an actual shell-verb keyword inside

the backticks (ls|rm|cat|curl|wget|sh|bash|zsh|chmod|chown|mv|cp|sudo|...|whoami|id|uname|...). Inline code references like ` variable_name , MAX_TOKENS , true , {"a":1} ` no longer match.

  1. Strip fenced code blocks before response-phase WAF scanning. Model

output legitimately contains `

\n…\n

blocks; the request-phase WAF already skips message content for the same reason (api/middleware/guardrails.ts:266). The response phase now mirrors that behavior via stripFencedCodeBlocks(). Un-fenced shell injection is still caught by the narrowed regex and the other patterns (cmdi_subshell, cmdi_pipe_chain, cmdi_redirect`, etc.).

Why this was missed

The original cmdi_backtick pattern was written under a request-phase mental model (user-supplied text rarely contains markdown fences). The WAF rule set was then promoted to phase: "both" for response-side detection without revisiting the false-positive profile. Response-side model output is dominated by markdown — and the original pattern matched any two-backtick span, including all of:

  • `
\n{}\n

` (fenced JSON)

  • ` variable_name ` (inline code reference)
  • ` {"a":1} ` (inline JSON-like content)

The OpenClaw stress harness submitted realistic agent prompts whose model responses contained fenced JSON blocks — caught immediately.

Removed interpreter keywords from cmdi_backtick

Removed: python, perl, ruby, php. Reason: a fenced code block tagged with the language (e.g. `

\ndef foo(): ...

) contains the literal interpreter name _as the fence language tag_, causing the narrowed regex to false-positive. Piped interpreter invocations like | python -c 'evil' remain caught by cmdi_pipe_chain (/\|\s*(cat|ls|id|whoami|curl|wget|nc|ncat|bash|sh|python|perl|ruby|php)\b/i`).

Defense-in-depth posture (after this change)

SurfaceDetection
Un-fenced backtick subshell with shell verbscmdi_backtick (narrowed)
Fenced code blocks in model outputstripped before scanning

| Fenced shell content (e.g. `

\nrm -rf /\n

` in output) | not flagged (low-realism for response-phase); request-phase WAF already runs on user input |

$(...) subshellscmdi_subshell
Pipe-chain to shell/interpretercmdi_pipe_chain
Shell metacharacter chains (;, &&, pipe)cmdi_semicolon / cmdi_logical

Files changed

  • src/security/waf-patterns.ts — narrowed cmdi_backtick regex (line ~213)
  • src/security/waf-text-normalizer.ts — NEW. Exports stripFencedCodeBlocks(text)
  • src/security/guardrail-checks/builtin/waf-injection.ts — calls

stripFencedCodeBlocks when context.phase === "response"

  • src/security/waf-patterns-fenced-code.test.ts — NEW. Unit tests for

legitimate-content non-match + actual-injection still-match

  • src/security/waf-text-normalizer.test.ts — NEW. Unit tests for fence-strip

Verification

pnpm test:fast -- src/security/waf-patterns-fenced-code.test.ts src/security/waf-text-normalizer.test.ts src/security/guardrail-checks/builtin/wave2-validation.test.ts → all pass. Full unit-test suite: 7,474 passed, 191 skipped (7,665 total).

Lockstep

  • SDKs: no API surface changed; no SDK updates needed
  • MCP: no tool surface changed
  • OpenAPI: no path changes
  • Docs: ship-log entry (this file)

OpenClaw stress repro

The repro that prompted this fix is captured at docs/assessment/openclaw-baseline-2026-05-20/repro/ and will be re-run as part of Phase 7 verification.