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:
- Narrow
cmdi_backtickto 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.
- 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)
| Surface | Detection |
|---|---|
| Un-fenced backtick subshell with shell verbs | cmdi_backtick (narrowed) |
| Fenced code blocks in model output | stripped 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 |
$(...) subshells | cmdi_subshell |
|---|---|
| Pipe-chain to shell/interpreter | cmdi_pipe_chain |
Shell metacharacter chains (;, &&, pipe) | cmdi_semicolon / cmdi_logical |
Files changed
src/security/waf-patterns.ts— narrowedcmdi_backtickregex (line ~213)src/security/waf-text-normalizer.ts— NEW. ExportsstripFencedCodeBlocks(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.