# 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][nvd]: "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`](semantic-kernel-filter-eval-rce.md) (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 |

[nvd]: https://nvd.nist.gov/vuln/detail/CVE-2026-44717

## 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

```python
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

```python
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

- [NVD CVE-2026-44717][nvd] (2026-05-15)
