Updated in v7.0.0 (breaking).
SQLVerifier.verify_sql() now returns a DiagnosticResult instead of an ad-hoc dict, and a proven-malicious query is reported as VERIFIED (truth) with a separate admission decision (policy). See the changelog for migration details.Overview
The SQL Engine validates queries for:- SQL injection patterns
- Destructive operations
- Schema compliance
- Syntax correctness
The DiagnosticResult contract
verify_sql() returns a DiagnosticResult with a status, an agent-safe agent_message, structured developer_fields, and a proof_ref bound to the query AST:
Proving that a query is malicious is a successful proof, so a malicious query is
VERIFIED — not BLOCKED. BLOCKED is reserved for cases where verification itself could not complete, and blocked results never carry a proof_ref. If the DDL schema fails to parse, the incomplete analysis is BLOCKED even when the query itself looks malicious (schema_parse_error takes precedence).
agent_message never leaks detection rules, rule IDs, or raw parser output. Rule-level detail lives in developer_fields.
Admission is a separate decision
Do not gate execution onstatus alone. POST /verify/sql returns an explicit admission field (ADMIT or BLOCKED) alongside the verdict. A malicious query is VERIFIED at the truth layer but BLOCKED at the admission layer. Gate on admission (or developer_fields.is_valid), never on status == "VERIFIED".
Usage
Schema compliance
When you passschema_ddl, every table the query reads or writes must be declared in that DDL. A reference to an undeclared table is an admission failure: the result is BLOCKED with is_valid: false and constraint_id: "sql_verifier.schema_violation". Names the query defines itself, such as CTEs, are not treated as tables. Without schema_ddl, the engine does not check table names.
With schema_ddl, the engine also expands SELECT * (qualified or not, such as SELECT u.*) against the declared columns. If the star covers a sensitive column, the query is not valid (is_valid: false) with a CRITICAL sensitive_column_access issue. Select the non-sensitive columns you need instead of *. Without schema_ddl, the engine can’t tell which columns a star covers.
Before v7.2.3, a query on a table outside the schema was admitted with a warning. See GHSA-q5g6-6cw2-cjm3.
MySQL and MariaDB executable comments
MySQL and MariaDB run SQL placed inside executable comments (/*! ... */, /*!NNNNN ... */, and /*M! ... */). The SQL engine expands these comments before analysis, so injection, destructive-operation, and schema checks apply to the SQL inside them. The engine expands them whatever dialect you declare.
developer_fields.executable_comments_expanded is true when the query contained an executable comment.
Injection detection
Detected patterns
Tautologies are detected by value, not by matching fixed text shapes. The engine constant-folds every column-free comparison, so
OR 1=1, OR 'a'='a', OR 1=1.0, OR 2>1, and arithmetic forms are all reported as injection_tautology. A WHERE or HAVING clause whose whole predicate folds to true, such as WHERE 1, is flagged too.
Changed in v9.0.0 (GHSA-jvhg-jw63-gr5g)
IN, IS, LIKE and ILIKE, BETWEEN, /, %, ||, COALESCE, CAST, ABS, and EXISTS over a SELECT with no FROM. AND and OR short-circuit on a constant operand, and a column compared with itself, such as id = id, is treated as always true (it is true for every non-NULL value). Filters such as id = 5 OR 1 IN (1), NOT (id = 5 AND 1 = 0) and id = 5 OR id = id are now tautologies: the query has is_valid: false and malicious_classification: true, and is not admitted.
A filter predicate that reads no column but can’t be worked out may still be always true, so the query is not admitted. The result is BLOCKED with is_valid: false, an issue of type unevaluated_constant_predicate, and constraint_id: "sql_verifier.unevaluated_predicate". When the same query also has a proven tautology, the tautology decides the result: VERIFIED with constraint_id: "sql_verifier.malicious" and malicious_classification: true. It doesn’t set malicious_classification, because nothing proves malice. Compare against a column or remove the constant condition; check that removing it doesn’t change which rows the query returns.
Stacked queries
A query that holds two or more statements, such asSELECT 1; DROP TABLE users, is reported as stacked_queries. The query is not valid, sets malicious_classification: true, and is not admitted.
The engine counts statements from the parsed query. A ; inside a string literal, a dollar-quoted string, or a comment is not a statement separator, so these queries are checked like any other single statement:
; before the end of the query as a separator. That applies when the query contains:
- A backslash anywhere. Escape rules depend on server settings, such as MySQL’s
NO_BACKSLASH_ESCAPES. --not followed by an ASCII whitespace character. MySQL starts a comment only then.- A
/*inside a block comment. - A carriage return not followed by a line feed.
;. The engine applies these checks both to the query as given and with MySQL executable comments expanded.
Before v9.0.0, any
; before the last character marked the query as stacked_queries, so a ; in a string or a trailing comment was refused.Subqueries and complexity limits
The engine checks queries with parenthesised subqueries, such asIN (SELECT ...), scalar subqueries, and subqueries in FROM, like any other query. A valid one is VERIFIED with is_valid: true.
developer_fields.complexity.subquery_depth reports how deeply the deepest SELECT is nested inside other SELECT, UPDATE, DELETE, INSERT, or MERGE statements. A plain query, and a UNION of two plain SELECT statements, report 0. A CTE counts as one level. When subquery_depth exceeds max_subquery_depth (default 3), the result is BLOCKED with constraint_id: "sql_verifier.complexity_limit_exceeded".
Before v9.0.0, every query with a parenthesised subquery was
BLOCKED with sql_verifier.execution_error, and the subquery depth limit never ran for those queries. A UNION of two plain SELECT statements reported a subquery_depth of 1.Destructive operations
Dangerous functions
New in v8.0.0 (GHSA-45jv-2j44-4c2q)
DANGEROUS_SQL_FUNCTIONS. It covers every dialect, whatever dialect you declare. A call anywhere in a statement, including inside a SELECT, is a CRITICAL issue with type dangerous_function. The query is not valid, sets malicious_classification: true, and is not admitted.
Before v8.0.0,
SQLVerifier admitted calls such as load_extension(), pg_read_file(), and xp_cmdshell(). QWEDClient.verify_sql also answered from the local SQLGuard instead of the server. It now always calls POST /verify/sql and returns UNVERIFIABLE when the server can’t be reached. See SQLGuard for the local pre-filter.Statements are classified across the whole query tree
The engine inspects every node of the parsed query, not just the top-level statement. A data-modifying statement hidden in a CTE or subquery is reported asdestructive_command, even though the outer statement is a SELECT:
admin_command wherever they appear (see the table below). Beyond that, only read-only query types are admitted at the top level: SELECT, UNION, INTERSECT, EXCEPT, and their WITH and parenthesized forms. Any other top-level statement is reported as non_read_only_statement with is_valid: false, so the engine fails closed on statement types it doesn’t recognize.
Administrative commands
The SQL engine also blocks administrative SQL commands by default, wherever they appear in the query:
These statement families are also reported as
CRITICAL admin_command issues: COPY (including COPY ... FROM PROGRAM), ATTACH, DETACH, PRAGMA, USE, LOAD DATA, ANALYZE, CACHE, UNCACHE, VACUUM, EXPORT, IMPORT, CALL, EXECUTE, and any statement the SQL parser doesn’t model.
Before v7.3.0, the engine checked only the top-level statement type and matched tautologies by text shape, and
SELECT * bypassed the sensitive-column check. See GHSA-6p94-4f75-g2wq and GHSA-6cpx-8jrq-r7h3.A resource-limit violation is a
CRITICAL admission failure but not evidence of malicious intent. Only true-malice issue types set malicious_classification: true.Supported dialects
- PostgreSQL
- MySQL
- SQLite
- SQL Server
- BigQuery