---
title: "GHSA-4v5r-g7f4-vvgc"
description: "SymPy allow-list bypass in qwed_sdk local math verification lets model output reach sympify() (incomplete fix for GHSA-xmm6-8r3x-j567). Affected and fixed versions of qwed."
url: https://qwedai.com/security/advisories/ghsa-4v5r-g7f4-vvgc
---

# GHSA-4v5r-g7f4-vvgc

SymPy allow-list bypass in qwed\_sdk local math verification lets model output reach sympify() (incomplete fix for GHSA-xmm6-8r3x-j567)

## Summary

`QWEDLocal.verify_math()` asks the configured LLM for a SymPy expression and evaluates it with `_safe_eval_sympy_expr()`. The validator rejects string arguments to SymPy functions that internally call `sympify()` (which uses `eval`). However, `_has_string_arg()` inspects only the **direct** positional and keyword arguments of a call. A string literal placed inside a container argument is not detected. It reaches `sympify()` and is evaluated outside the SDK allow-list and outside the CodeGuard check.

GHSA-xmm6-8r3x-j567 hardened the **answer** path (`_math_answers_match` → `safe_parse_expr`). The **expression** path (`qwed_sdk/qwed_local.py`, `verify_math`, call to `_safe_eval_sympy_expr`) still accepts this form.

## Details

-   `qwed_sdk/qwed_local.py` `_has_string_arg()` (~L328): loops over `node.args` / `node.keywords` only.
-   `_is_safe_sympy_ast()` permits `ast.Tuple` / `ast.List` / `ast.Constant(str)` anywhere in the tree.
-   `_evaluate_sympy_ast()` then calls e.g. `sympy.simplify(<tuple containing str>)`, and SymPy sympifies each element.

### Benign proof (no dangerous payload used)

`factorial` is **not** in `_ALLOWED_SYMPY_FUNCS`:

| Input to `_safe_eval_sympy_expr` | Result |
| --- | --- |
| `sympy.simplify(factorial(5))` | `DisallowedExpressionError` (control) |
| `sympy.simplify(("factorial(5)",))` | **accepted → `(120,)`** |
| `sympy.simplify(("binomial(10, 3)",))` | **accepted → `(120,)`** |

So an arbitrary string reaches SymPy's string evaluator, and names outside the allow-list execute.

## Impact

An attacker who can influence the LLM response used by `QWEDLocal.verify_math()` can feed attacker-chosen text to `sympify()` on the user's machine. That is the same sink class as GHSA-xmm6-8r3x-j567. In addition, the local verifier can be made to report a value derived from an expression outside the allow-list.

## Patches

-   Walk the whole call subtree (`ast.walk`) and reject any `str` constant unless it is the **direct** argument of `Symbol`/`symbols`/`Rational`/`Integer`.
-   Reject `ast.Tuple`/`ast.List` containing non-numeric constants for every function in `_SYMPY_FUNCS_REJECTING_STRING_ARGS`.
-   Regression tests: the three rows above, plus nested list/tuple/keyword forms, must raise `DisallowedExpressionError`.

## Workarounds

Use the server-side `/verify/math` (which uses `safe_parse_expr`) instead of `QWEDLocal.verify_math()` with untrusted or third-party LLM endpoints.
