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 overnode.args/node.keywordsonly._is_safe_sympy_ast()permitsast.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 anystrconstant unless it is the direct argument ofSymbol/symbols/Rational/Integer. - Reject
ast.Tuple/ast.Listcontaining 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.