Skip to content

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

frozenset({"eval", "exec", "compile", "__import__", "getattr", "parse_expr"})

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-server versions 0.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 via extra_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