critical · 9.8CVE-2026-55546Aug 25, 2026

CVE-2026-55546: qwed-mcp Remote Code Execution via Unsafe SymPy parse_expr()

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

The math verification function in qwed-mcp passes user-supplied strings straight into SymPy's expression parser, which internally calls Python's eval() with no sandbox, letting any caller run…

Packageqwed-mcp
Ecosystempip
Affected< 0.2.1
Fixed in0.2.1
CVE-2026-55546: qwed-mcp Remote Code Execution via Unsafe SymPy parse_expr()

The problem

The public function verify_math_expression() in src/qwed_mcp/engines/math_engine.py forwards both the expression and claimed_result arguments directly to sympy.parsing.sympy_parser.parse_expr(). No allowlist, AST validation, or sandboxing is applied before either call.

SymPy's parse_expr() internally calls Python's built-in eval(). Because no global_dict is set, Python injects the full built-in namespace automatically, making __import__, open, exec, and every other built-in available inside the evaluated string. An unauthenticated caller who controls either argument achieves arbitrary OS command execution as the process user, confirmed as root inside Docker.

Proof of concept

A working proof-of-concept for CVE-2026-55546 in qwed-mcp, with the exact payload below.

python
__import__('os').system('id > /tmp/out.txt && hostname >> /tmp/out.txt && touch /tmp/pwned')

The local_dict passed to parse_expr() adds math symbols (x, y, z, pi, e) but does not set __builtins__ to an empty dict. Python's eval() contract is: when globals is omitted or lacks __builtins__, the interpreter silently injects the current module's full builtins.

That makes __import__ available, so the payload above resolves and executes before SymPy ever tries to interpret the return value as an expression.

The patch (commit 362e618) fixes both sites by adding a pre-validation step and passing global_dict={"__builtins__": {}} to every parse_expr() call. It also adds an AST walker (_validate_math_syntax) that raises ValueError on any node type, name, or function not in a strict allowlist, blocking the attack even if the __builtins__ restriction were somehow bypassed.

Root cause is CWE-94 (Improper Control of Generation of Code).

The fix

Upgrade qwed-mcp to v0.2.1. The fix in commit 362e618 adds global_dict={"__builtins__": {}} to both parse_expr() calls and introduces _validate_math_syntax(), an AST allowlist walker that rejects any expression containing non-math nodes, unknown names, or unapproved function calls before the string ever reaches parse_expr().

Reporter not attributed.

References: [1][2][3][4][5]

Related research