Skip to content

fix(math): restrict sympy expression parsing#200

Merged
Rahul Dass (rahuldass19) merged 8 commits into
QWED-AI:mainfrom
sebastiondev:fix/cwe95-main-sympy-9383
Jun 14, 2026
Merged

fix(math): restrict sympy expression parsing#200
Rahul Dass (rahuldass19) merged 8 commits into
QWED-AI:mainfrom
sebastiondev:fix/cwe95-main-sympy-9383

Conversation

@sebastiondev

@sebastiondev Sebastion (sebastiondev) commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

QWED Enforcement Checklist

  • No fallback execution added
  • No new raw eval / exec usage introduced
  • Verification is enforced before execution
  • No silent error handling or bypass-oriented retries added
  • No trust placed in LLM-provided expected values, reasoning, or confidence
  • Failure paths remain fail-closed

Summary

This change fixes a CWE-95 code injection issue in math expression verification. User-controlled expression strings from the POST /verify/math endpoint, VerificationEngine math helpers, SemanticValidator, and math batch verification were passed directly to SymPy parse_expr(). SymPy's parser evaluates generated Python code internally, so calling it without restricted namespaces allows a tenant-supplied expression to reach Python execution primitives.

The affected public endpoint is authenticated with get_current_tenant, but that is not a sufficient mitigation for a multi-tenant API: any tenant with a valid API key can submit math verification input, while a valid API key should not grant arbitrary code execution on the service host.

The fix adds qwed_new.core.safe_parser.safe_parse_expr() and replaces the bare parser calls in the affected math paths. The wrapper rejects dangerous constructs before parsing, supplies an allow-listed local namespace containing expected SymPy math symbols/functions only, removes Python builtins from the parser globals, and enforces basic input validation including empty/non-string checks and a length limit. This preserves deterministic symbolic verification while narrowing the parser boundary to math expressions.

Vulnerability details

  • CWE: CWE-95, Improper Neutralization of Directives in Dynamically Evaluated Code
  • Severity: high, because a crafted math expression can execute host commands under the API process account
  • Affected files/functions before this patch:
    • src/qwed_new/api/main.py, verify_math, request expression -> parse_expr()
    • src/qwed_new/core/verifier.py, VerificationEngine math/identity/derivative/integral/limit helpers -> parse_expr()
    • src/qwed_new/core/batch.py, math batch item query -> parse_expr()
    • src/qwed_new/core/validator.py, semantic math validation -> parse_expr()

Proof of Concept

The underlying unsafe behavior can be reproduced against the previous implementation with the same sink used by the affected code paths:

from sympy.parsing.sympy_parser import parse_expr

# This executes the host command before returning a SymPy value.
parse_expr('__import__("os").system("id")')

For the API path, a tenant with a valid key could send the malicious expression to the existing math verification route:

import requests

response = requests.post(
    "http://localhost:8000/verify/math",
    headers={"x-api-key": "<valid tenant api key>"},
    json={"expression": "__import__(\"os\").system(\"id\")"},
    timeout=10,
)
print(response.status_code)
print(response.text)

After this patch, the same payload is rejected by safe_parse_expr() before SymPy evaluation. Normal expressions such as 2+2, x**2 + 2*x + 1, sin(x), sqrt(16), and factorial(5) still parse successfully.

Validation

I validated the changed parser boundary and the endpoint exception path with:

PYTHONPATH=src python3 -m pytest tests/security/test_safe_parser.py -q
PYTHONPATH=src python3 -m pytest tests/test_api_exceptions.py::test_verify_math_exception_handling -q

Results:

  • tests/security/test_safe_parser.py: 36 passed
  • tests/test_api_exceptions.py::test_verify_math_exception_handling: 1 passed

I also checked for remaining direct production uses of SymPy parse_expr() outside the new wrapper. The remaining sympy.parse_expr text is in documentation/example text, not an active parser call.

A broader local run of tests/security/test_safe_parser.py tests/test_api_exceptions.py passed the new security tests but exposed an unrelated Python 3.14 compatibility failure in tests/test_api_exceptions.py::test_verify_stats_exception_handling caused by ast.Num removal while importing stats_verifier.py. That failure is outside this parser change.

Security analysis

The exploit works because parse_expr() is not just a passive math grammar parser: it transforms input and evaluates Python code. Without a constrained global_dict and local_dict, identifiers such as __import__ can resolve to Python runtime capabilities and invoke OS commands. The request body field expression and the corresponding engine/batch inputs are controlled by the caller, so the attacker controls the string that reaches the evaluation sink.

This patch mitigates the issue in depth. The denylist blocks common Python execution, reflection, import, filesystem, and process-spawning constructs before parsing. The __builtins__ removal prevents fallback access to builtins during evaluation. The allow-listed local dictionary limits successful names to expected mathematical symbols, constants, and SymPy functions. The length/type checks also keep malformed or oversized inputs fail-closed instead of reaching the parser.

Before submitting, I attempted to disprove the issue by checking whether authentication or routing protections would remove exploitability. The route does require get_current_tenant, but the project exposes tenant API keys for verification workloads; that precondition does not already grant code execution. I also checked for existing input validation before the parser and did not find a sanitizer that would block the payload before it reached parse_expr().

Notes

This remains compliant with QWED's deterministic verification boundary: the patch does not add fallback execution, retries, or LLM trust. It tightens the symbolic math parser so verification continues to run through SymPy, but only with a constrained math namespace.


Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

Summary by CodeRabbit

  • New Features
    • Added a hardened mathematical expression parsing layer with input validation and safer variable handling, supporting approved symbols/constants and math functions.
  • Bug Fixes
    • Routed math verification (including equality checking and calculus/limit handling) and validation through the safer parsing pipeline to better resist malformed or hostile inputs.
  • Tests
    • Added security-focused tests for denylisted constructs, length/depth limits, variable validation, and parser isolation.
    • Updated API exception-handling tests to match the new parsing behavior.

sympy parse_expr uses Python code execution internally and without
restrictions on local_dict/global_dict allows arbitrary code execution
through crafted math expression strings.

Add safe_parse_expr wrapper that:
- Validates input against a denylist of dangerous patterns (dunder
  attrs, import, exec, os, subprocess, etc.)
- Restricts the namespace to only known-safe sympy objects (math
  functions, constants, and common symbolic variables)
- Strips Python builtins from the global namespace
- Enforces a maximum expression length

Replace all bare parse_expr calls across the codebase:
- src/qwed_new/api/main.py (/verify/math endpoint)
- src/qwed_new/core/verifier.py (VerificationEngine)
- src/qwed_new/core/batch.py (batch math verification)
- src/qwed_new/core/validator.py (SemanticValidator)

Signed-off-by: Sebastion <sebastion@sebastion.dev>
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR mitigates CWE-95 (code injection via SymPy's parse_expr) across four production call sites by introducing qwed_new.core.safe_parser.safe_parse_expr() as the single authorized parser entry point, replacing all direct parse_expr calls in main.py, verifier.py, batch.py, and validator.py.

  • safe_parser.py implements layered defenses: denylist regex for dangerous Python constructs, __builtins__ removal from the eval global dict, an allowlisted local_dict covering single-letter variables, Greek letters, math constants, and SymPy functions, plus AST/tree depth limits and strict sympy.Expr result-type validation.
  • verifier.py additionally replaces bare Symbol(variable) with get_safe_symbol(variable), ensuring the integration variable in derivative/integral/limit operations passes the same length and denylist checks as expression strings.
  • tests/security/test_safe_parser.py adds 36 security-focused tests; the patch target in test_api_exceptions.py is updated to match the new import path.

Confidence Score: 5/5

Safe to merge; the CWE-95 fix is correctly applied across all four affected call sites with well-structured defense-in-depth.

All bare parse_expr calls in production paths are replaced. The new wrapper combines a denylist, stripped builtins, an allowlisted namespace, and a strict result-type check — each layer independently limits the exploit surface. The lambda gap noted in safe_parser.py has no reachable exploit path under current constraints and does not affect correctness of the math verification paths.

safe_parser.py — the denylist in _DENYLIST_PATTERN is the core security boundary; the lambda keyword omission is worth a follow-up hardening pass.

Important Files Changed

Filename Overview
src/qwed_new/core/safe_parser.py New hardened wrapper for SymPy parsing: adds denylist, stripped builtins, allowlisted local namespace, AST/tree depth limits, and type validation; lambda keyword not in denylist.
src/qwed_new/core/verifier.py All parse_expr call sites replaced with safe_parse_expr; Symbol(variable) replaced with get_safe_symbol(variable) across derivative, integral, and limit helpers.
src/qwed_new/core/batch.py Math batch verification path (identity equality and single-expression branches) now routed through safe_parse_expr.
src/qwed_new/core/validator.py SemanticValidator syntax check now uses safe_parse_expr; straightforward one-line swap.
src/qwed_new/api/main.py API endpoint verify_math swaps bare parse_expr for safe_parse_expr; outer exception handler sanitizes errors to INTERNAL_VERIFICATION_ERROR before response.
tests/security/test_safe_parser.py New 36-test security suite covering denylist, Greek variables, input validation, namespace isolation, AST depth, and extra_symbols; no test for lambda expressions.
tests/test_api_exceptions.py Patch target updated from sympy.parsing.sympy_parser.parse_expr to qwed_new.core.safe_parser.parse_expr; correctly verifies sanitized error response.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User-supplied expression string] --> B[safe_parse_expr]
    B --> C{Type check\nstr?}
    C -->|No| ERR1[SafeParserError]
    C -->|Yes| D{Empty or\ntoo long?}
    D -->|Yes| ERR2[SafeParserError]
    D -->|No| E{Denylist\nregex match?}
    E -->|Match| ERR3[SafeParserError\ndisallowed construct]
    E -->|No match| F[AST depth check]
    F -->|Exceeds limit| ERR4[SafeParserError]
    F -->|OK| G[Build safe local_dict\nSymPy symbols + functions only]
    G --> H[global_dict = copy of template\n__builtins__: empty dict]
    H --> I[parse_expr with\nlocal_dict + global_dict]
    I --> J{Result is\nsympy.Expr?}
    J -->|No| ERR5[SafeParserError]
    J -->|Yes| K{SymPy tree\ndepth OK?}
    K -->|Exceeds limit| ERR6[SafeParserError]
    K -->|OK| L[Return sympy.Expr]
Loading

Reviews (8): Last reviewed commit: "fix: validate extra_symbols keys against..." | Re-trigger Greptile

Comment thread src/qwed_new/core/safe_parser.py Outdated
Comment thread src/qwed_new/core/safe_parser.py
Comment thread src/qwed_new/core/verifier.py
@rahuldass19 Rahul Dass (rahuldass19) added the bug Something isn't working label Jun 12, 2026
@rahuldass19

Copy link
Copy Markdown
Member

Thanks for the report, Sebastion (@sebastiondev). We independently audited the codebase against your claims and verified the vulnerability.

Vulnerability Status: Confirmed

We reproduced the exploit on our current SymPy 1.14.0 installation using a chr()-based bypass that works with and without SymPy transformations:

from sympy.parsing.sympy_parser import parse_expr
parse_expr('__import__(chr(111)+chr(115)).system(chr(105)+chr(100))')
# Executes os.system("id") on the host

All 17 production parse_expr calls across main.py, verifier.py, batch.py, and validator.py are reachable from user-controlled input with zero sanitization between the request body and the eval() sink. Authentication (get_current_tenant) is not a mitigation here — any tenant with a valid API key can exploit this.

The fix direction is correct and aligns with QWED's fail-closed philosophy. However, we need the following changes before we can merge:

Required Changes

1. Multi-letter symbolic variable support

_build_safe_local_dict only defines single-letter variables (x, y, z, etc.). Expressions using alpha, beta, theta, phi, omega, lambda, epsilon, tau, delta, sigma, gamma — which are common in our verification workloads — will raise ValueError where they previously parsed fine. These need to be added to the allow-list.

2. Shared mutable _SAFE_GLOBAL_DICT

The module-level dict is passed by reference to every parse_expr call. SymPy transformations can mutate global_dict in-place, which means entries from one tenant's parse call can leak into subsequent calls. Use dict(_SAFE_GLOBAL_DICT) (shallow copy) per invocation.

3. Validate variable parameter in calculus methods

verify_derivative, verify_integral, and verify_limit route expression through safe_parse_expr but pass the variable argument directly to Symbol(variable) without any validation. The hardened boundary should be consistent across all user-controlled string inputs.


If you're able to address these, we're happy to re-review. If not, we may implement the fix internally based on your approach — the vulnerability is real and we want to close it promptly. Either way, thank you for the responsible report.

@codspeed-hq

codspeed-hq Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 54.57%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 18 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
test_bench_math_algebraic_expression 1.8 ms 1.1 ms +56.65%
test_bench_math_simple_arithmetic 1.8 ms 1.2 ms +52.52%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing sebastiondev:fix/cwe95-main-sympy-9383 (70f0978) with main (06801f6)

Open in CodSpeed

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.53125% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/qwed_new/core/safe_parser.py 95.04% 5 Missing ⚠️
src/qwed_new/api/main.py 50.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR hardens math expression parsing across the verification system by introducing a denylist-based safe parser that replaces SymPy's unrestricted parse_expr. The safe parser validates input length, rejects dangerous constructs via denylist patterns, enforces AST depth limits, and restricts the parsing namespace to allow-listed symbols and functions, mitigating CWE-95 code injection risks. The hardened parser is then integrated across the core verification engine, semantic validator, and batch/API services with comprehensive security test coverage.

Changes

Safe Expression Parser Security Hardening

Layer / File(s) Summary
Safe parser security implementation and exported API
src/qwed_new/core/safe_parser.py
Introduces safe_parse_expr() with denylist validation, AST-depth checking, allow-listed symbol namespace, and safe global dictionary restricted from built-ins. Adds SafeParserError exception class, validate_variable_name() for identifier validation and sanitization, and get_safe_symbol() for consistent symbol generation matching the safe namespace.
Core verification engine integration
src/qwed_new/core/verifier.py
Updates all verification methods (verify_math, verify_identity, verify_derivative, verify_integral, verify_limit) to parse expressions via safe_parse_expr() and generate variable symbols via get_safe_symbol(), replacing direct SymPy parse_expr and Symbol construction.
Semantic validator integration
src/qwed_new/core/validator.py
Updates SemanticValidator syntax validation to parse expressions via safe_parse_expr() instead of SymPy's unrestricted parser.
Batch and API service expression parsing
src/qwed_new/core/batch.py, src/qwed_new/api/main.py
Updates BatchVerificationService._verify_item and /verify/math endpoint to parse expressions via safe_parse_expr() in both equation (left/right sides) and non-equation verification paths.
API exception test infrastructure update
tests/test_api_exceptions.py
Updates exception injection point to patch the new safe parser's parse function, maintaining validation that the API sanitizes parse errors without leaking exception details.
Comprehensive security test coverage
tests/security/test_safe_parser.py
Adds parameterized denylist rejection tests, legitimate expression acceptance tests, variable name validation tests, namespace isolation tests, AST depth limit tests, and extra_symbols constraint tests to ensure all safe parser security boundaries are consistently enforced.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰 A parser once wild, now tamed with care,
No more dangerous code shall lurk there!
With denylists strong and depths to constrain,
Safe math expressions shine bright again! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(math): restrict sympy expression parsing' accurately and concisely summarizes the main change: restricting SymPy expression parsing to address a code injection vulnerability.
Description check ✅ Passed The PR description comprehensively covers all template sections: enforcement checklist (all items checked), detailed summary of the CWE-95 fix, vulnerability details, proof of concept, validation approach, security analysis, and notes on QWED compliance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sebastiondev

Copy link
Copy Markdown
Contributor Author

Thank you Rahul Dass (@rahuldass19) for the thorough review and for independently confirming the vulnerability — really appreciated. All three requested changes have been addressed in bbf9ade:

1. Multi-letter symbolic variable support ✅

_build_safe_local_dict now includes the full Greek alphabet (lowercase: alpha, beta, gamma, delta, epsilon, zeta, eta, theta, iota, kappa, mu, nu, xi, omicron, rho, sigma, tau, upsilon, phi, chi, psi, omega) plus commonly-used capitals (Alpha, Beta, Gamma, Delta, Theta, Lambda, Sigma, Phi, Psi, Omega). All are pre-defined as Symbol(...) in the allow-list, so expressions like alpha + beta or sin(theta) now parse correctly. Test coverage added in TestSafeParseExprMultiLetterSymbols.

2. Shared mutable _SAFE_GLOBAL_DICT → shallow copy per call ✅

safe_parse_expr now passes dict(_SAFE_GLOBAL_DICT) instead of the module-level dict directly. This prevents SymPy transformations from mutating the shared reference across invocations, eliminating the cross-tenant namespace leak risk. Test coverage added in TestSafeParseExprGlobalDictIsolation.

3. variable parameter validation in calculus methods ✅

Added validate_variable_name() to safe_parser.py — it applies a character-set whitelist (^[A-Za-z][A-Za-z0-9_]{0,49}$), the same denylist regex, and a 50-char length cap. verify_derivative, verify_integral, and verify_limit in verifier.py now call validate_variable_name(variable) before Symbol(variable), making the hardened boundary consistent across all user-controlled string inputs. Test coverage added in TestValidateVariableName.


All 61 security tests pass (the pre-existing test_verify_stats_exception_handling failure is an unrelated ast.Num deprecation in Python 3.14, not caused by this PR).

Happy to adjust if any of the Greek letter selections need tweaking for your workloads.

Comment thread src/qwed_new/core/verifier.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
tests/security/test_safe_parser.py (1)

126-138: ⚡ Quick win

Strengthen the global dict isolation test.

The current test only verifies that multiple calls succeed without exceptions, but it doesn't validate the actual isolation mechanism. It doesn't prove that copying global_dict per call prevents cross-contamination of SymPy transformations or symbols.

Consider testing a scenario where state leakage would actually occur if the dict were shared:

  • Parse an expression with extra_symbols containing a custom symbol
  • Parse a second expression referencing that symbol name without passing extra_symbols
  • Verify the second parse fails (proving the symbol didn't leak)

Alternatively, if such leakage is difficult to trigger in practice, document why the current test provides sufficient coverage.

♻️ Example strengthened test
 def test_global_dict_not_shared_between_calls(self) -> None:
-    """Parse two different expressions and confirm no cross-contamination."""
-    safe_parse_expr("x + 1")
-    safe_parse_expr("alpha + beta")
-    # If global_dict were shared mutably, SymPy transformations could
-    # leak symbols from one call into another. The shallow-copy fix
-    # prevents this. We just verify no exception is raised and both
-    # parse independently.
-    result = safe_parse_expr("y + 2")
-    assert str(result) == "y + 2"
+    """Verify that extra_symbols from one call don't leak into another."""
+    from sympy import Symbol
+    
+    # First call with a custom symbol
+    safe_parse_expr("custom_var + 1", extra_symbols={"custom_var": Symbol("custom_var")})
+    
+    # Second call should NOT have access to custom_var
+    with pytest.raises(ValueError):
+        safe_parse_expr("custom_var + 2")  # Should fail: custom_var not in default namespace
+    
+    # Verify normal expressions still work
+    result = safe_parse_expr("x + 2")
+    assert str(result) == "x + 2"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/security/test_safe_parser.py` around lines 126 - 138, Update the test
for global dict isolation to actively demonstrate leakage wouldn't occur: call
safe_parse_expr once with extra_symbols including a custom symbol (e.g.,
safe_parse_expr("leak + 1", extra_symbols={"leak": Symbol("leak")})) and assert
that parsing that expression succeeds and produces the expected result, then
call safe_parse_expr again for the same symbol name without providing
extra_symbols and assert that this second call raises the appropriate error
(e.g., NameError or SympifyError) or does not return a Symbol — this proves that
the per-call copy of _SAFE_GLOBAL_DICT prevents the first call's symbol from
leaking into subsequent calls; update
TestSafeParseExprGlobalDictIsolation.test_global_dict_not_shared_between_calls
accordingly, keeping references to safe_parse_expr and _SAFE_GLOBAL_DICT to
locate the implementation if needed.
src/qwed_new/core/safe_parser.py (2)

198-203: 💤 Low value

Including Symbol in local_dict is necessary but warrants documentation.

Symbol is needed because SymPy's transformations may emit code referencing Symbol(). However, this allows users to create symbols with arbitrary names via expressions like Symbol('anything'). The denylist protects against dangerous attribute accesses on the resulting symbol, but consider adding a comment documenting this security boundary.

         # Sympy internal types emitted by standard_transformations
         "Integer": Integer,
         "Float": Float,
         "Rational": Rational,
+        # Symbol is required by transformations; denylist prevents dangerous
+        # attribute access on created symbols.
         "Symbol": Symbol,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qwed_new/core/safe_parser.py` around lines 198 - 203, Add a short
in-source comment by the local_dict entry that maps "Symbol" (the dictionary
that includes Integer, Float, Rational, Symbol used with
standard_transformations) explaining why Symbol is required (SymPy
transformations may emit Symbol()), the security boundary it creates (users can
call Symbol('name') to create arbitrary symbol names) and that the existing
denylist/attribute-access protections are relied on to mitigate risks; keep the
comment concise and reference the local_dict and Symbol so future readers know
this is intentional and what to review if tightening is needed.

40-66: 💤 Low value

Denylist-based filtering is a reasonable mitigation but inherently weaker than allowlist.

The pattern set covers key attack vectors (dunders, code execution primitives, system modules). However, denylist approaches can be bypassed by novel attack vectors or encoding tricks. Since SymPy's parse_expr ultimately uses eval(), consider adding defense-in-depth measures in future iterations:

  • AST complexity/depth limits
  • Symbolic expression tree validation post-parse

For this PR, the combination of denylist + restricted namespace + stripped builtins is a substantial improvement and aligns with fail-closed philosophy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qwed_new/core/safe_parser.py` around lines 40 - 66, The denylist in
_DANGEROUS_PATTERNS is useful but brittle; add defense‑in‑depth by (1) enforcing
an AST complexity/depth limit on parsed expressions (e.g., run ast.parse on the
input and reject if node count/depth exceeds a safe threshold) before calling
SymPy's parse_expr, and (2) validating the resulting SymPy expression tree after
parse_expr to ensure it contains only allowed node/symbol types (reject
Function, Call, or unexpected Name nodes). Update the code paths that call
parse_expr/use _DANGEROUS_PATTERNS to perform the pre-parse AST checks and the
post-parse symbolic validation (leave the denylist in place as an additional
filter).

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/qwed_new/core/safe_parser.py`:
- Around line 198-203: Add a short in-source comment by the local_dict entry
that maps "Symbol" (the dictionary that includes Integer, Float, Rational,
Symbol used with standard_transformations) explaining why Symbol is required
(SymPy transformations may emit Symbol()), the security boundary it creates
(users can call Symbol('name') to create arbitrary symbol names) and that the
existing denylist/attribute-access protections are relied on to mitigate risks;
keep the comment concise and reference the local_dict and Symbol so future
readers know this is intentional and what to review if tightening is needed.
- Around line 40-66: The denylist in _DANGEROUS_PATTERNS is useful but brittle;
add defense‑in‑depth by (1) enforcing an AST complexity/depth limit on parsed
expressions (e.g., run ast.parse on the input and reject if node count/depth
exceeds a safe threshold) before calling SymPy's parse_expr, and (2) validating
the resulting SymPy expression tree after parse_expr to ensure it contains only
allowed node/symbol types (reject Function, Call, or unexpected Name nodes).
Update the code paths that call parse_expr/use _DANGEROUS_PATTERNS to perform
the pre-parse AST checks and the post-parse symbolic validation (leave the
denylist in place as an additional filter).

In `@tests/security/test_safe_parser.py`:
- Around line 126-138: Update the test for global dict isolation to actively
demonstrate leakage wouldn't occur: call safe_parse_expr once with extra_symbols
including a custom symbol (e.g., safe_parse_expr("leak + 1",
extra_symbols={"leak": Symbol("leak")})) and assert that parsing that expression
succeeds and produces the expected result, then call safe_parse_expr again for
the same symbol name without providing extra_symbols and assert that this second
call raises the appropriate error (e.g., NameError or SympifyError) or does not
return a Symbol — this proves that the per-call copy of _SAFE_GLOBAL_DICT
prevents the first call's symbol from leaking into subsequent calls; update
TestSafeParseExprGlobalDictIsolation.test_global_dict_not_shared_between_calls
accordingly, keeping references to safe_parse_expr and _SAFE_GLOBAL_DICT to
locate the implementation if needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b93953c5-3fb4-47f0-93ea-c266f7c3684a

📥 Commits

Reviewing files that changed from the base of the PR and between 06801f6 and bbf9ade.

📒 Files selected for processing (7)
  • src/qwed_new/api/main.py
  • src/qwed_new/core/batch.py
  • src/qwed_new/core/safe_parser.py
  • src/qwed_new/core/validator.py
  • src/qwed_new/core/verifier.py
  • tests/security/test_safe_parser.py
  • tests/test_api_exceptions.py

- Add get_safe_symbol() to return consistent Symbol objects matching
  safe_parse_expr namespace (fixes 'n' variable mismatch in calculus)
- Add AST depth limit and post-parse sympy.Basic type validation
- Add comment documenting Symbol in allow-list boundary
- Strengthen global dict isolation test
- Update verifier.py calculus methods to use get_safe_symbol
Comment thread src/qwed_new/core/verifier.py Fixed
Comment thread src/qwed_new/core/safe_parser.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/qwed_new/core/safe_parser.py`:
- Around line 127-131: The extra_symbols processing loop currently silently
ignores non-SymPy values instead of failing closed. In the loop over
extra_symbols.items(), modify the code so that when a value is not an instance
of Symbol or sympy.Basic, raise an appropriate exception (such as ValueError or
TypeError) with a clear error message indicating which key contains an invalid
value. This ensures caller mistakes are surfaced immediately rather than
silently suppressed, maintaining fail-closed behavior at this security boundary.
- Around line 178-179: The exception handler in the except block is raising
ValueError with raw, unsanitized parser exception text. Replace this ValueError
with SafeParserError to provide a consistent boundary exception type for parser
failures. Sanitize or generalize the error message instead of directly including
the raw exception details (the exc variable). This ensures sensitive information
is not exposed and maintains a deterministic, fail-closed error boundary as per
the coding guidelines.
- Around line 56-61: The _check_ast_depth function has a fail-open vulnerability
where SyntaxError exceptions (which occur for implicit multiplication syntax
like "sin sin x") cause early return without depth checking. Instead of
returning on SyntaxError, attempt to parse the expression using SymPy's
implicit_multiplication_application transformation and validate the resulting
AST depth. Additionally, in the extra_symbols validation (around lines 127-131),
replace the silent filtering of non-sympy.Basic values with explicit error
raising to enforce fail-closed behavior. Finally, at line 179, replace the
generic exception wrapping that includes raw exception text with a sanitized
error message that does not leak parser internals, following QWED error message
guidelines.

In `@tests/security/test_safe_parser.py`:
- Around line 189-190: The test for deep expression parsing is too permissive by
allowing both SafeParserError and ValueError to be raised. To enforce
fail-closed behavior explicitly, modify the pytest.raises call for the
safe_parse_expr test to only expect SafeParserError, removing ValueError from
the exception tuple. This ensures that if a ValueError is raised instead of the
expected SafeParserError when deep_expr is parsed, the test will fail and catch
boundary regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1f7a3e8c-63f5-4334-95dc-15178e0d6ff7

📥 Commits

Reviewing files that changed from the base of the PR and between bbf9ade and e1454de.

📒 Files selected for processing (4)
  • .coverage
  • src/qwed_new/core/safe_parser.py
  • src/qwed_new/core/verifier.py
  • tests/security/test_safe_parser.py

Comment thread src/qwed_new/core/safe_parser.py
Comment thread src/qwed_new/core/safe_parser.py
Comment thread src/qwed_new/core/safe_parser.py Outdated
Comment thread tests/security/test_safe_parser.py Outdated
- Remove unused validate_variable_name import from verifier.py (CodeQL)
- Fail-closed on SyntaxError in _check_ast_depth (Sentry HIGH)
- Raise SafeParserError for invalid extra_symbols (CodeRabbit)
- Use SafeParserError with sanitized message (CodeRabbit)
- Tighten tests to only expect SafeParserError

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/qwed_new/core/safe_parser.py`:
- Around line 61-63: The SafeParserError being raised in the SyntaxError handler
is missing exception chaining suppression. Add `from None` to the raise
SafeParserError statement to suppress the exception chain, matching the pattern
used at line 185 and preventing parser internals from leaking. This will make
the code consistent with other error paths in the file and satisfy the Ruff B904
rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8c4f92e-0c23-44e6-b792-546a3ea4fac3

📥 Commits

Reviewing files that changed from the base of the PR and between e1454de and 356577d.

📒 Files selected for processing (3)
  • src/qwed_new/core/safe_parser.py
  • src/qwed_new/core/verifier.py
  • tests/security/test_safe_parser.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/security/test_safe_parser.py

Comment thread src/qwed_new/core/safe_parser.py Outdated
Comment thread src/qwed_new/core/safe_parser.py
- Skip pre-parse AST depth for implicit-mult syntax (fixes 2x regression)
- Add post-parse SymPy expression tree depth check (catches all syntax)
Comment thread src/qwed_new/core/safe_parser.py Outdated
- e is commonly used as a free symbol; only uppercase E maps to Euler's number
- Removes silent semantic change from original parse_expr behavior
Comment thread src/qwed_new/core/safe_parser.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/qwed_new/core/safe_parser.py (1)

64-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when the pre-parse validator rejects the input.

Lines 64-65 swallow SyntaxError and continue into parse_expr(). Even with _sympy_tree_depth, that is still a syntax-dependent fallback: one class of inputs is validated before parsing, while another proceeds after a failed verification step. At this boundary, QWED rules require a sanitized failure instead of continuing on a later “safe enough” check.

Suggested minimal hardening
     try:
         tree = ast.parse(expression, mode="eval")
     except SyntaxError:
-        return
+        raise SafeParserError(
+            "Expression uses syntax that cannot be validated for safety"
+        ) from None

As per coding guidelines, "Fail-closed security boundary: if verification/parsing fails, block and do not continue execution on “safe enough”" and "No fallback execution: do not add any backup parsing/execution path."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qwed_new/core/safe_parser.py` around lines 64 - 65, The exception handler
for SyntaxError at lines 64-65 silently returns instead of failing closed as
required by QWED security guidelines. When the pre-parse validator rejects the
input and raises a SyntaxError, the code must not continue execution or allow
fallback parsing. Replace the silent return statement in the except SyntaxError
block with an exception that propagates the validation failure and prevents any
further execution or fallback parsing attempts through parse_expr().

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/qwed_new/core/safe_parser.py`:
- Around line 95-106: The _validate_sympy_result function currently accepts any
sympy.Basic type, which includes relational expressions like `x < y` that are
not arithmetic expressions. This causes TypeError downstream when code attempts
arithmetic operations on these relationals. Change the isinstance check in
_validate_sympy_result to validate against sympy.Expr instead of sympy.Basic to
restrict the validation to arithmetic expressions only and enforce the
arithmetic-only contract at the parser boundary.

---

Duplicate comments:
In `@src/qwed_new/core/safe_parser.py`:
- Around line 64-65: The exception handler for SyntaxError at lines 64-65
silently returns instead of failing closed as required by QWED security
guidelines. When the pre-parse validator rejects the input and raises a
SyntaxError, the code must not continue execution or allow fallback parsing.
Replace the silent return statement in the except SyntaxError block with an
exception that propagates the validation failure and prevents any further
execution or fallback parsing attempts through parse_expr().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab0955f6-361e-4142-aad9-47325ff675c6

📥 Commits

Reviewing files that changed from the base of the PR and between 356577d and 430686b.

📒 Files selected for processing (1)
  • src/qwed_new/core/safe_parser.py

Comment thread src/qwed_new/core/safe_parser.py
- Preserve original parse error messages for domain error detection
- Check sympy.Expr instead of sympy.Basic to reject relationals (x < y)
Comment thread src/qwed_new/core/safe_parser.py
- Keys must be strings
- Keys checked against _DENYLIST_PATTERN before being used
@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@rahuldass19

Copy link
Copy Markdown
Member

Hey Sebastion (@sebastiondev), thanks again for the report and the initial fix proposal. The core direction was correct, and we built directly on top of it.

Given the severity of the issue (confirmed RCE affecting verification paths), we chose to complete the remaining hardening work directly in-tree so we could review, test, release, and coordinate disclosure without delay.

The final remediation includes additional fixes and tests covering:

  • n symbol handling in calculus verification
  • AST depth limits and post-parse SymPy tree validation
  • extra_symbols validation
  • Relational expression rejection (x < y)
  • Exception message hygiene
  • Additional security regression tests

Your report and remediation proposal were instrumental in identifying and closing the vulnerability family, and we’ll ensure appropriate credit is reflected in the advisory and release notes.

Appreciate the contribution and the responsible disclosure.

@rahuldass19 Rahul Dass (rahuldass19) merged commit 6066b68 into QWED-AI:main Jun 14, 2026
24 of 25 checks passed
@sebastiondev

Copy link
Copy Markdown
Contributor Author

Appreciate the review — thanks for getting it merged.

@sebastiondev

Copy link
Copy Markdown
Contributor Author

Thanks for the merge. Glad it landed cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants