Audit findings: BUG-R2-S1-A4-H1 + BUG-R2-S1-A4-H3 | CWE-184 / CWE-94 | CVSS 7.7 | PoC: reproduced, EXP confirmed
Summary
All three AST matchers behind the advertised OWASP LLM06 gate (_is_safe_code) test only structural root names, so a dangerous module reached through any reference they never inspect reaches the container:
_dangerous_import (secure_code_executor.py:84-96) checks only the first dotted segment against 7 roots {os, sys, subprocess, socket, urllib, requests, http} -> import importlib, import posix (CPython built-in; posix.system is what os.system delegates to), import pandas.io.common all pass.
_dangerous_attribute (:98-107) walks a chain to its innermost base and flags only ast.Name bases in that set -> pd.io.common.os.system(...), np.lib.npyio.os.system(...), pc.os.system(...), posix.system(...) pass; Call-rooted chains (importlib.import_module('os').system(...)) fail the isinstance check and pass.
_dangerous_call (:110-118) matches only 8 builtins -> system, popen, import_module pass.
The CodeVerifier layer never compensates (os.system is WARNING-only; is_safe = critical_count == 0). The gadgets provably resolve at sandbox runtime: the wrapper pre-imports numpy as np (:410) and pandas as pd (:428); pandas/io/common.py and numpy/lib/npyio.py both do module-level import os in the pinned ranges. Aggravating: the pre-7.0.0 substring fallback contained os. — the "AST-aware" rewrite is strictly weaker for every indirection form.
Delivery paths
- Consensus (
/verify/consensus high/maximum): only pre-gate is CodeVerifier.verify_code — every gadget form passes.
- Stats (
/verify/stats): the LLM output is the code with zero output validation (translate_stats); the import-free pd.* forms additionally pass RestrictedExecutor.is_code_safe. In-band exfiltration through observed_result in the HTTP response, persisted to the audit DB; EXP-measured full-fidelity multi-MB channel.
Impact
os-module operations (command execution, filesystem primitives, env dump) run as uid 0 inside the sandbox despite the advertised no-os/no-subprocess contract; host-tempdir writes and symlink planting via the rw bind; container limits bound but do not prevent. Breaks the "only validated code executes" guarantee.
Suggested fix
- Extend roots:
posix, nt, importlib, ctypes, builtins; check every dotted segment, not just the first.
- Flag any attribute chain containing a dangerous module name anywhere in the chain.
- Add dangerous call names:
system, popen, import_module, execv(e), spawnv(e), fork(pty).
- Stats path: reject deep attribute chains rooted at sandbox module aliases (
pd, np, json, sys) — legit stats code only needs one attribute level.
- Durable direction: allowlist model (resolve module roots; permit only pd/np/df public APIs).
See audit patch sketches for both files.
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 findings:
BUG-R2-S1-A4-H1+BUG-R2-S1-A4-H3| CWE-184 / CWE-94 | CVSS 7.7 | PoC: reproduced, EXP confirmedSummary
All three AST matchers behind the advertised OWASP LLM06 gate (
_is_safe_code) test only structural root names, so a dangerous module reached through any reference they never inspect reaches the container:_dangerous_import(secure_code_executor.py:84-96) checks only the first dotted segment against 7 roots{os, sys, subprocess, socket, urllib, requests, http}->import importlib,import posix(CPython built-in;posix.systemis whatos.systemdelegates to),import pandas.io.commonall pass._dangerous_attribute(:98-107) walks a chain to its innermost base and flags onlyast.Namebases in that set ->pd.io.common.os.system(...),np.lib.npyio.os.system(...),pc.os.system(...),posix.system(...)pass; Call-rooted chains (importlib.import_module('os').system(...)) fail theisinstancecheck and pass._dangerous_call(:110-118) matches only 8 builtins ->system,popen,import_modulepass.The CodeVerifier layer never compensates (
os.systemis WARNING-only;is_safe = critical_count == 0). The gadgets provably resolve at sandbox runtime: the wrapper pre-importsnumpy as np(:410) andpandas as pd(:428);pandas/io/common.pyandnumpy/lib/npyio.pyboth do module-levelimport osin the pinned ranges. Aggravating: the pre-7.0.0 substring fallback containedos.— the "AST-aware" rewrite is strictly weaker for every indirection form.Delivery paths
/verify/consensushigh/maximum): only pre-gate isCodeVerifier.verify_code— every gadget form passes./verify/stats): the LLM output is the code with zero output validation (translate_stats); the import-freepd.*forms additionally passRestrictedExecutor.is_code_safe. In-band exfiltration throughobserved_resultin the HTTP response, persisted to the audit DB; EXP-measured full-fidelity multi-MB channel.Impact
os-module operations (command execution, filesystem primitives, env dump) run as uid 0 inside the sandbox despite the advertised no-os/no-subprocess contract; host-tempdir writes and symlink planting via the rw bind; container limits bound but do not prevent. Breaks the "only validated code executes" guarantee.
Suggested fix
posix,nt,importlib,ctypes,builtins; check every dotted segment, not just the first.system,popen,import_module,execv(e),spawnv(e),fork(pty).pd,np,json,sys) — legit stats code only needs one attribute level.See audit patch sketches for both files.
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.