GHSA-6p94-4f75-g2wq
SQLVerifier certifies destructive and administrative SQL as VERIFIED/ADMIT (DML hidden in CTEs; incomplete statement-type denylist)
highPublished 16 October 2026
Impact
SQLVerifier.verify_sql returns status=VERIFIED, developer_fields.is_valid=True and admission ADMIT for two classes of query that its own checks reject in plain form. A consumer that gates on the admission decision (the documented primary use, including POST /verify/sql) is told the query is "safe to execute".
1. Data-modifying CTEs. _check_command_type classifies a query only by the type of the root AST node. A data-modifying statement wrapped in a CTE — WITH d AS (DELETE FROM t ... RETURNING *) SELECT * FROM d — has a Select root, so it is certified VERIFIED/ADMIT, while the bare DELETE is correctly BLOCKED. On PostgreSQL the CTE runs to completion even if never referenced. UPDATE and INSERT variants behave the same.
2. Incomplete statement-type denylist. DESTRUCTIVE_COMMANDS/ADMIN_COMMANDS are two hand-maintained sets of root classes. Many statement families are in neither set and pass with zero issues and ADMIT, including COPY ... FROM PROGRAM, COPY ... FROM/TO <file>, ATTACH/DETACH (SQLite, the default dialect), PRAGMA, ANALYZE, CACHE/UNCACHE, LOAD DATA, EXPORT DATA. Depending on the consumer's database and privileges, these range from server-side command execution and arbitrary file read to settings changes.
Affected
All releases through 7.2.3. Reproduced at the verdict level on main @ a4d6714 (v7.2.3), across the sqlite, postgres and mysql dialects, with and without schema_ddl. The 7.2.3 schema_table_missing change blocks only a few sub-variants, and only when schema_ddl is supplied (/verify/sql requires it; /verify/batch defaults it to empty, skipping the check).
Details
- Root-type-only classification:
src/qwed_new/core/sql_verifier.py_check_command_type(around lines 392-414), with the two denylist sets (around 103-127). critical_count == 0then yieldsis_valid=True/VERIFIED;admission_decision(src/qwed_new/core/diagnostics.py:1114-1136) maps that to ADMIT.guards/sql_guard.pyalready walks the whole tree withfind(), but the verification path does not use it.
Fix
Classify over the whole parse tree, not just the root node, and move from a denylist to an allowlist of read-only statement types (plus explicit handling of CTEs that contain DML and of statement families such as COPY/ATTACH/PRAGMA). Fail closed on any statement type not on the allowlist.
Source
OpenVuln external audit (2026-10); findings BUG-R2-C2-A2-H1 and the command-type policy finding. Reported privately; not previously disclosed.
Affected and fixed
- Package
- qwed
- Affected
- <= 7.2.3
- Fixed in
- 7.3.0
- Severity
- high
- Weakness
- CWE-184 Incomplete List of Disallowed InputsCWE-693 Protection Mechanism Failure