MCP STDIO command-injection guard (v0.7.6+, carried from 2026-05-11)¶
agent_airlock.mcp_spec.stdio_command_injection_guard.StdioCommandInjectionGuard
is the runtime gate for shell metachars and path traversal in MCP
STDIO argv vectors.
Why¶
Per Help Net Security 2026-05-05 citing Snyk ToxicSkills:
"1 in 4 MCP servers opens AI agent security to code execution risk."
MCP STDIO transport accepts an argv vector that often arrives via the model's tool-call payload. A shell metachar in any element opens an injection path. The guard denies on:
- Shell metachars in any element of
commandorargs:;,&&,||,|, newline, carriage return, backtick,$(. - Path traversal (
../resolving outside an operator-suppliedcwd_allowlist). The traversal check is opt-in — empty allowlist disables it.
Install¶
Core. No optional extra. The mcp package is not loaded — the
guard is a regex / string-set match over the argv.
Quickstart¶
from agent_airlock import StdioCommandInjectionGuard, StdioCommandInjectionVerdict
# Default: metachar block-list active; traversal check opt-in.
guard = StdioCommandInjectionGuard(cwd_allowlist=("/srv/app",))
decision = guard.evaluate({"command": "bash", "args": ["-c", "echo hi; rm -rf /"]})
# decision.allowed is False
# decision.verdict == StdioCommandInjectionVerdict.DENY_SHELL_METACHAR
# decision.matched_metachar == ";"
Companion preset¶
agent_airlock.policy_presets.mcp_stdio_command_injection_preset_defaults()
returns the recommended config dict (preset_id, severity,
default_action, advisory_url, cwd_allowlist, extra_metachars).
Decision shape¶
evaluate(args) returns StdioCommandInjectionDecision. The
allowed: bool field intentionally mirrors the v0.6.1 –v0.7.5
family for chain-friendly composition.
| Verdict | When |
|---|---|
ALLOW |
no metachar in argv; traversal check disabled or path inside allowlist |
DENY_SHELL_METACHAR |
any default or operator-extended metachar found |
DENY_PATH_TRAVERSAL |
path-shaped argv element resolves outside cwd_allowlist |
Extending the metachar set¶
guard = StdioCommandInjectionGuard(
cwd_allowlist=("/srv/app",),
extra_metachars=frozenset({"#"}), # treat `#` as a metachar too
)
Honest scope¶
- The default metachar set captures the disclosed exploitation primitives. Determined attackers can sometimes shell-quote around individual metachars in narrow contexts.
- Operators with a fixed-binary policy should ALSO use
enforce_allowlist/AllowlistVerdictas a second layer — the runtime allowlist gate refuses anything outside a signed manifest, regardless of metachar shape. - The traversal check is opt-in because operators who don't
route their MCP servers through a fixed cwd would see false
positives. Pass a non-empty
cwd_allowlistto enable it.