SQL Classification
The classifier is governed by the AXIS Security Classification Contract v1.0 — an engineering binding contract between the Classifier and the Policy Evaluator. Any deviation is a Critical Security Defect (Authorization Bypass) and must be treated as such.
Contract rules. "Any deviation from this contract is a Critical Security Defect and must be treated as an active Authorization Bypass." The contract is "not valid until proven by automated tests in CI" — human review alone is insufficient.
Classification principles
- Semantic > Syntax — classification reflects the actual database state change, not the first keyword.
- State Mutation = Write — any statement that creates, alters, deletes or moves persistent data or metadata is
Write. - Fail-Closed on Ambiguity — if the semantic effect cannot be determined from the AST, default to
WriteorBlock. Never default toReadunless proven read-only. - Defense in Depth — risk signals are decision inputs; they can override a permissive read default only as a fallback when the Classifier is incorrect.
- Deterministic — same SQL under same context always produces the same classification.
- CI-Enforced — all rules are enforced by automated tests in CI.
QueryType: the primary classification
Primary classification values: QueryType::Read, QueryType::Write, QueryType::Unknown.
| Type | Definition | Examples |
|---|---|---|
Read | Does not modify any persistent or session state; only returns data | SELECT (without INTO, volatile functions, or data-modifying CTEs); SHOW, EXPLAIN, DESCRIBE; COPY TO STDOUT; SELECT with IMMUTABLE/STABLE functions only; LISTEN/NOTIFY (minimal state change — audit separately) |
Write | Creates, alters, deletes or moves persistent data, schema, sequences or large objects — including anything that would change the result of a subsequent SELECT on the same table | See mandatory patterns below |
Unknown | Semantic effect cannot be determined from the AST | Treated as Write or Block — never as Read |
Mandatory Write patterns
| Statement | Classification |
|---|---|
INSERT / UPDATE / DELETE | Write (DML) |
SELECT INTO | Write (DDL + DML) |
CREATE TABLE AS (CTAS) | Write (DDL + DML) |
MERGE | Write (DML) |
TRUNCATE | Write (DDL) |
DROP / ALTER | Write (DDL) |
REFRESH MATERIALIZED VIEW | Write (DML) |
COPY FROM | Write (DML) |
COPY TO PROGRAM | Critical – always BLOCK |
CALL (procedure) / DO (anonymous block) | Write (DML) unless proved read-only |
nextval() / setval() | Sequence mutation (Write) |
lo_import / lo_export | Large object mutation (Write) |
ALTER SYSTEM | Critical – always BLOCK |
Data-modifying CTE (WITH d AS (DELETE ...) SELECT * FROM d) | Write (DML — the write happens before the outer read) |
| Nested / chained CTE writes | Write (DML) |
Unknown / ambiguous handling
VOLATILEorSECURITY DEFINERfunction →WriteorBlockunless explicitly allowlisted.- Function volatility unknown →
Write(fail-closed). - Procedure (
CALL) or anonymous block (DO) →Write. COPY PROGRAM→Blockalways.
Mandatory risk signals (fallback layer)
Risk signals are a defense-in-depth fallback, not the primary control. They override a permissive read default only when the classifier is wrong:
| Signal | Detected when | Override behavior |
|---|---|---|
select_into | SELECT INTO present | If QueryType == Read (classifier error) → Block or RequireApproval |
cte_write | Data-modifying CTE present | Same override |
sequence_write | nextval()/setval() present | Same override |
volatile_function | VOLATILE function call present | Same override |
copy_program | COPY TO PROGRAM present | Always Block |
Other risk signals used by policy evaluation: prod_write, ddl_operation, bulk_operation, delete_without_where, unknown_target, unknown_scope, cross_schema.
Evaluator behavior
Read QueryType.
Read RiskSignals.
Primary path: if QueryType == Write, apply defaults.write rules directly.
Fallback path: if QueryType == Read and a critical risk signal exists, log CLASSIFIER_OVERRIDE_TRIGGERED and apply the override rule.
Never rely on the override as the primary protection.
CI-enforced testing
- Positive tests: assert
QueryType == Write+ expected signal. - Negative tests: assert
QueryType == Read+ no critical signal. - Override tests: with
defaults.read = ALLOW, critical-signal cases must still yield BLOCK or REQUIRE_APPROVAL. - Mutation tests ("Survival Proof"): removing a rule (e.g.
IntoClausedetection) must make the test fail red; restoring it passes green. - Evasion corpus in
tests/evasion_corpus/: comments, dollar strings, Unicode confusables, mixed encodings, multi-statement payloads, extended-protocol parameter bypass attempts.
Classification-related errors
Fail-closed code PARSE_REJECTED_UNTRUSTED_INPUT covers unsupported SQL, invalid UTF-8, raw NUL bytes and unparseable extended-protocol bind values. Related structured errors: empty_sql, multi_statement_rejected, unsupported_sql_shape, parser_error, parser_unsupported_syntax, unsafe_read_shape. See the Error Codes reference.
Related: Request Lifecycle · Policy Engine · Native PG Wire Protocol (SQL feature matrix for the wire path)