Eval-RCE guard (CVE-2026-44717, v0.8.0+)¶
agent_airlock.mcp_spec.eval_rce_guard.EvalRCEGuard is the runtime
detector for the bare-eval RCE class disclosed by NVD on
2026-05-15 (CVE-2026-44717, MCP Calculate Server < 0.1.1).
Why¶
Per NVD CVE-2026-44717: "MCP Calculate Server is a
mathematical calculation service based on MCP protocol and SymPy
library. Prior to 0.1.1, the use of eval() to evaluate mathematical
expressions without proper input sanitization leads to remote code
execution."
The exploit class is not MCP-Calculate-specific. Any tool that
passes a model-derived string to eval() / exec() / compile() /
sympy.parsing.sympy_parser.parse_expr() without pinning
local_dict / global_dict is vulnerable.
This guard is complementary to v0.7.5 FilterEvalRCEGuard:
| Guard | Targets |
|---|---|
FilterEvalRCEGuard (v0.7.5) |
lambda / Expression.Lambda<> / {{ eval(...) }} syntax shapes in known filter fields |
EvalRCEGuard (v0.8.0, this page) |
Bare eval( / exec( / compile( / __import__( / getattr( / parse_expr( invocations in any string arg |
Install¶
Core. No optional extra. The sympy / mcp-calculate-server packages
are not imported — the guard is a compiled-regex pass plus a
tuple-membership check.
Quickstart¶
from agent_airlock import EvalRCEGuard, EvalRCEVerdict
guard = EvalRCEGuard() # default sink set + curated vulnerable-package denylist
decision = guard.evaluate({"expression": "eval('__import__(\"os\").system(\"id\")')"})
# decision.allowed is False
# decision.verdict == EvalRCEVerdict.DENY_EVAL_SINK
# decision.matched_sink == "eval"
What's in the default sink set¶
The guard uses word-boundary regex so a benign string like
user.name == 'Eval Industries' does NOT trigger a deny.
parse_expr safe-form exemption¶
parse_expr(s, local_dict={}) or parse_expr(s, global_dict={}) —
the upstream-patched safe form from CVE-2026-44717 — is allowed.
The detector inspects the same parenthesised arg list for either
local_dict= or global_dict= keyword.
Vulnerable-package denylist¶
The guard ships with a curated denylist of (package_name, version)
tuples drawn from public advisories. Today's seed:
mcp-calculate-serverversions0.0.8,0.0.9,0.1.0
When a tool arg carries server_package + server_version matching
any entry, the guard returns DENY_VULNERABLE_PACKAGE. Operators
extend via extra_vulnerable_packages=(("name", "version"), ...).
Companion preset¶
agent_airlock.policy_presets.stdio_guard_eval_defaults_2026_05_15()
returns the recommended config dict — parity with v0.7.5's
semantic_kernel_filter_eval_rce_2026_25592_26030_defaults.
Decision shape¶
evaluate(args) returns EvalRCEDecision. The allowed: bool
field mirrors the v0.6.1–v0.7.x decision family for chain-friendly
composition.
| Verdict | When |
|---|---|
ALLOW |
no eval-sink and no vulnerable-package match |
DENY_EVAL_SINK |
bare-eval sink invocation detected in a string arg |
DENY_VULNERABLE_PACKAGE |
server_package + server_version matches the denylist |
Honest scope¶
- Regex heuristic. Catches the disclosed CVE class and obvious
obfuscation variants (namespace prefix like
sympy.parse_expr, whitespace before(). Does NOT catch an attacker who controls the surrounding context and hides the sink behind an indirection (locals()["eval"](...)). That variant is named explicitly viaextra_sinks=frozenset({"locals", ...}). - Vulnerable-package list is curated. Operators are responsible for keeping the denylist current as new CVEs in this class drop.
Primary source¶
- NVD CVE-2026-44717 (2026-05-15)