Audit finding: BUG-R2-S1-A4-H2 | CWE-94 | CVSS 7.7 | PoC: reproduced, EXP confirmed
Summary
TranslationLayer._validate_math_output() (translator.py:84-108) — the documented OWASP LLM05 gate before the expression is interpolated into result = {expression} Python code — validates characters, not shape. Three gaps:
- The safe-charset
^[0-9+\-*/().\sa-z]+$ (translator.py:99) includes \s, which matches newlines; the pre-match replace(' ', '') strips only literal spaces. Multi-line expressions pass.
- The
dangerous_keywords denylist (translator.py:90) omits import (it lists __import__, which requires double underscores).
- Neither the Pydantic schema (
MathVerificationTask.expression) nor _generate_verification_code (consensus_verifier.py:922, raw f"result = {task.expression}") enforces a single-expression structure.
A multi-line expression becomes multi-statement module code that also passes both downstream gates (pattern scanner is warning-only for os.system; executor AST gate blocklists only 7 module roots / 8 builtins).
Proof of concept
Provider returns expression:
1
import pandas.io.common as pc
pc.os.system(chr(105) + chr(100))
-> generated code result = 1\nimport pandas.io.common as pc\npc.os.system(chr(105)+chr(100)) executes in the sandbox. import posix / posix.system(...) variant triggers zero scanner matches. Engines run in parallel, so execution happens during the engine phase before any verdict.
Delivery: attacker-influenced provider output (default local Ollama, operator CUSTOM_BASE_URL, MITM'd provider — deterministic) or prompt injection via tenant query (probabilistic). Container execution requires the bare-metal deployment envelope; the gate acceptance itself occurs in every mode.
Code anchors
| File |
Line |
Symbol |
src/qwed_new/core/translator.py |
90/99 |
denylist missing import; charset admits newlines |
src/qwed_new/core/schemas.py |
34 |
no single-expression constraint |
src/qwed_new/core/consensus_verifier.py |
922/679/542 |
raw interpolation; engine phase |
src/qwed_new/core/secure_code_executor.py |
330 |
AST gate (companion issue) |
Suggested fix
Structural gate in _validate_math_output: ast.parse(task.expression, mode="eval") — reject anything that is not exactly one Python expression (statements, imports, multi-line input fail by construction). See audit patch sketch.
Related: executor gate bypass issue (module-indirection gadgets), OWASP LLM05/LLM06.
Source: OpenVuln external audit of QWED-AI/qwed-verification (snapshot v7.0.0, re-verified against current main v7.1.0). All code anchors below were spot-checked against current main before filing.
Audit finding:
BUG-R2-S1-A4-H2| CWE-94 | CVSS 7.7 | PoC: reproduced, EXP confirmedSummary
TranslationLayer._validate_math_output()(translator.py:84-108) — the documented OWASP LLM05 gate before the expression is interpolated intoresult = {expression}Python code — validates characters, not shape. Three gaps:^[0-9+\-*/().\sa-z]+$(translator.py:99) includes\s, which matches newlines; the pre-matchreplace(' ', '')strips only literal spaces. Multi-line expressions pass.dangerous_keywordsdenylist (translator.py:90) omitsimport(it lists__import__, which requires double underscores).MathVerificationTask.expression) nor_generate_verification_code(consensus_verifier.py:922, rawf"result = {task.expression}") enforces a single-expression structure.A multi-line expression becomes multi-statement module code that also passes both downstream gates (pattern scanner is warning-only for
os.system; executor AST gate blocklists only 7 module roots / 8 builtins).Proof of concept
Provider returns expression:
-> generated code
result = 1\nimport pandas.io.common as pc\npc.os.system(chr(105)+chr(100))executes in the sandbox.import posix/posix.system(...)variant triggers zero scanner matches. Engines run in parallel, so execution happens during the engine phase before any verdict.Delivery: attacker-influenced provider output (default local Ollama, operator
CUSTOM_BASE_URL, MITM'd provider — deterministic) or prompt injection via tenantquery(probabilistic). Container execution requires the bare-metal deployment envelope; the gate acceptance itself occurs in every mode.Code anchors
src/qwed_new/core/translator.pyimport; charset admits newlinessrc/qwed_new/core/schemas.pysrc/qwed_new/core/consensus_verifier.pysrc/qwed_new/core/secure_code_executor.pySuggested fix
Structural gate in
_validate_math_output:ast.parse(task.expression, mode="eval")— reject anything that is not exactly one Python expression (statements, imports, multi-line input fail by construction). See audit patch sketch.Related: executor gate bypass issue (module-indirection gadgets), OWASP LLM05/LLM06.
Source: OpenVuln external audit of QWED-AI/qwed-verification (snapshot v7.0.0, re-verified against current
mainv7.1.0). All code anchors below were spot-checked against current main before filing.