Every CVE Search/ Get API key

CVE-2026-55585

QWED: Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`

8.8 HighFix available

QWED is open-source AI verification infrastructure for deterministic verification of LLM outputs, tool calls, code, schemas, and agent state before production execution. Prior to 5.1.2, the qwed package passes caller-controlled math expressions directly to SymPy parse_expr() without restricted global_dict and local_dict namespaces, allowing Python eval() to resolve builtins and execute arbitrary Python code in the API server process. In src/qwed_new/api/main.py, POST /verify/math is protected by get_current_tenant but accepts any valid tenant API key, reads the expression field, applies only a cosmetic re.sub(r'(\d)(()', r'\1*\2', expression) normalization, and passes the result to parse_expr(). In src/qwed_new/core/batch.py, POST /verify/batch sends math items through batch_service.create_job(), stores item.query verbatim, and _verify_item() passes VerificationType.MATH input to parse_expr() without sanitization. The default-enabled POST /auth/signup endpoint allows anyone to create a standard tenant account, POST /auth/api-keys issues an x-api-key, and either vulnerable path can then be used to read or write files, modify data, execute operating system commands, terminate the service, and compromise other tenants in a shared deployment. This issue is fixed in version 5.1.2.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
Low
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
High

Exploit likelihood

0.78%Up 0.3 points since Aug 26, 2026
90%50%10%1%

What each source reports

CVE List Score used 8.8

QWED: Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`

QWED is open-source AI verification infrastructure for deterministic verification of LLM outputs, tool calls, code, schemas, and agent state before production execution. Prior to 5.1.2, the qwed package passes caller-controlled math expressions directly to SymPy parse_expr() without restricted global_dict and local_dict namespaces, allowing Python eval() to resolve builtins and execute arbitrary Python code in the API server process. In src/qwed_new/api/main.py, POST /verify/math is protected by get_current_tenant but accepts any valid tenant API key, reads the expression field, applies only a cosmetic re.sub(r'(\d)(()', r'\1*\2', expression) normalization, and passes the result to parse_expr(). In src/qwed_new/core/batch.py, POST /verify/batch sends math items through batch_service.create_job(), stores item.query verbatim, and _verify_item() passes VerificationType.MATH input to parse_expr() without sanitization. The default-enabled POST /auth/signup endpoint allows anyone to create a standard tenant account, POST /auth/api-keys issues an x-api-key, and either vulnerable path can then be used to read or write files, modify data, execute operating system commands, terminate the service, and compromise other tenants in a shared deployment. This issue is fixed in version 5.1.2.

Record
CVE-2026-55585
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94
Updated
2026-11-02
EUVDEUVD-2026-65580 8.8

QWED is open-source AI verification infrastructure for deterministic verification of LLM outputs, tool calls, code, schemas, and agent state before production execution. Prior to 5.1.2, the qwed package passes caller-controlled math expressions directly to SymPy parse_expr() without restricted global_dict and local_dict namespaces, allowing Python eval() to resolve builtins and execute arbitrary Python code in the API server process. In src/qwed_new/api/main.py, POST /verify/math is protected by get_current_tenant but accepts any valid tenant API key, reads the expression field, applies only a cosmetic re.sub(r'(\d)(()', r'\1*\2', expression) normalization, and passes the result to parse_expr(). In src/qwed_new/core/batch.py, POST /verify/batch sends math items through batch_service.create_job(), stores item.query verbatim, and _verify_item() passes VerificationType.MATH input to parse_expr() without sanitization. The default-enabled POST /auth/signup endpoint allows anyone to create a standard tenant account, POST /auth/api-keys issues an x-api-key, and either vulnerable path can then be used to read or write files, modify data, execute operating system commands, terminate the service, and compromise other tenants in a shared deployment. This issue is fixed in version 5.1.2.

Record
EUVD-2026-65580
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Updated
2026-11-02
GitHub AdvisoriesGHSA-q27q-98j4-9pfv 8.8

qwed Vulnerable to Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`

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:

# 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:

# 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)

# 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)

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…

Record
GHSA-q27q-98j4-9pfv
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94
Updated
2026-11-02
NVD 8.8

QWED is open-source AI verification infrastructure for deterministic verification of LLM outputs, tool calls, code, schemas, and agent state before production execution. Prior to 5.1.2, the qwed package passes caller-controlled math expressions directly to SymPy parse_expr() without restricted global_dict and local_dict namespaces, allowing Python eval() to resolve builtins and execute arbitrary Python code in the API server process. In src/qwed_new/api/main.py, POST /verify/math is protected by get_current_tenant but accepts any valid tenant API key, reads the expression field, applies only a cosmetic re.sub(r'(\d)(()', r'\1*\2', expression) normalization, and passes the result to parse_expr(). In src/qwed_new/core/batch.py, POST /verify/batch sends math items through batch_service.create_job(), stores item.query verbatim, and _verify_item() passes VerificationType.MATH input to parse_expr() without sanitization. The default-enabled POST /auth/signup endpoint allows anyone to create a standard tenant account, POST /auth/api-keys issues an x-api-key, and either vulnerable path can then be used to read or write files, modify data, execute operating system commands, terminate the service, and compromise other tenants in a shared deployment. This issue is fixed in version 5.1.2.

Record
CVE-2026-55585
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94
Updated
2026-11-02
OSV 8.8

QWED: Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`

QWED is open-source AI verification infrastructure for deterministic verification of LLM outputs, tool calls, code, schemas, and agent state before production execution. Prior to 5.1.2, the qwed package passes caller-controlled math expressions directly to SymPy parse_expr() without restricted global_dict and local_dict namespaces, allowing Python eval() to resolve builtins and execute arbitrary Python code in the API server process. In src/qwed_new/api/main.py, POST /verify/math is protected by get_current_tenant but accepts any valid tenant API key, reads the expression field, applies only a cosmetic re.sub(r'(\d)(()', r'\1*\2', expression) normalization, and passes the result to parse_expr(). In src/qwed_new/core/batch.py, POST /verify/batch sends math items through batch_service.create_job(), stores item.query verbatim, and _verify_item() passes VerificationType.MATH input to parse_expr() without sanitization. The default-enabled POST /auth/signup endpoint allows anyone to create a standard tenant account, POST /auth/api-keys issues an x-api-key, and either vulnerable path can then be used to read or write files, modify data, execute operating system commands, terminate the service, and compromise other tenants in a shared deployment. This issue is fixed in version 5.1.2.

Record
CVE-2026-55585
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94
Updated
2026-11-02
OSVGHSA-q27q-98j4-9pfv 8.8

qwed Vulnerable to Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`

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:

# 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:

# 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)

# 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)

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…

Record
GHSA-q27q-98j4-9pfv
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94
Updated
2026-11-02
OSVPYSEC-2026-3914 8.8

qwed Vulnerable to Authenticated Remote Code Execution via Unsafe SymPy `parse_expr()`

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:

# 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:

# 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)

# 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)

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…

Record
PYSEC-2026-3914
CVSS 3.1
8.8 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
qwed
PyPI
before 5.1.2
5.1.2
qwed
PyPI
before 5.1.2, plus listed versions
0.1.02.0.02.0.12.1.02.2.0
Show 9 moreShow less
2.3.02.4.02.4.13.0.14.0.04.0.15.0.05.1.05.1.1
5.1.2

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-55585 is a high vulnerability (CVSS 8.8) in Qwed-ai Qwed-verification, published August 25, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.78%, the 56th percentile. Fixed in qwed 5.1.2.

References

This page doesn't list everything

Left out here: the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open