Skip to content

[Security] SecureCodeExecutor AST gate bypass via module indirection (pd.io.common.os / posix / importlib) #336

Description

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:

  1. _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.
  2. _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.
  3. _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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical priority — blocks trust boundary completionbugSomething isn't workingexternal-auditFindings from external security auditsqwed securityqwed securitysecuritySecurity finding from adversarial auditverificationverification

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions