sql-guard-statements
SQL guard: only read statements reach the backend
The sections below are generated from the CLI (methodology 0.3-draft, CLI 0.3.0); the same text is printed by sealmark explain sql-guard-statements.
- pack
sql-guardopt-in pack- weight
- criticalcandidate
- scope
- server (judged by what reached the harness's upstream stub, not by the server's reply; no SQL is parsed)
- decided by
- upstream
- since
- 0.3-draft (methodology 0.3-draft)
- config keys
checks.sql-guard-statements.control_sqlchecks.sql-guard-statements.enabledchecks.sql-guard-statements.severitysql_toolsupstream.kind
Probes
controlSELECT * FROM {table} WHERE 'sm-{token}' = 'sm-{token}' (with no exact scope.allow entry: SELECT 1 AS sm_{token}; override with checks.sql-guard-statements.control_sql); it must reach the upstream, else the check is inconclusive coreinsertINSERT INTO {table} (sm_{token}) VALUES ('sm-{token}') (writes; must not reach the upstream)updateUPDATE {table} SET sm_{token} = 'sm-{token}' WHERE 1 = 0 (writes; must not reach the upstream)deleteDELETE FROM {table} WHERE sm_{token} = 'sm-{token}' (writes; must not reach the upstream)mergeMERGE INTO {table} AS sm_{token} USING (SELECT 'sm-{token}' AS v) AS s ON 1 = 0 WHEN NOT MATCHED THEN INSERT (v) VALUES ('sm-{token}') (writes; must not reach the upstream)createCREATE TABLE sm_{token} (v text DEFAULT 'sm-{token}') (DDL; must not reach the upstream)dropDROP TABLE IF EXISTS sm_{token} /* sm-{token} */ (DDL; must not reach the upstream)alterALTER TABLE {table} ADD COLUMN sm_{token} text DEFAULT 'sm-{token}' (DDL; must not reach the upstream)truncateTRUNCATE TABLE {table} /* sm_{token} sm-{token} */ (DDL; must not reach the upstream)grantGRANT SELECT ON {table} TO sm_{token} /* sm-{token} */ (privileges; must not reach the upstream)vacuumVACUUM {table} /* sm_{token} sm-{token} */ (maintenance; must not reach the upstream)analyzeANALYZE {table} /* sm_{token} sm-{token} */ (maintenance; must not reach the upstream)set-sessionSET SESSION sm_{token} = 'sm-{token}'; SELECT 1 (session state; must not reach the upstream)copyCOPY {table} (sm_{token}) TO '/tmp/sm-{token}' (file access; must not reach the upstream)stackedSELECT 'sm-{token}'; DROP TABLE sm_{token} (multiple statements; must not reach the upstream)cte-dmlWITH d AS (DELETE FROM {table} WHERE sm_{token} = 'sm-{token}' RETURNING *) SELECT * FROM d (DML hidden in a CTE; must not reach the upstream)explain-analyze-writeEXPLAIN ANALYZE INSERT INTO {table} (sm_{token}) VALUES ('sm-{token}') (EXPLAIN ANALYZE of a write; must not reach the upstream)obfuscated/*x*/ iNsErT INTO {table} (sm_{token}) VALUES ('sm-{token}') (mixed case and a leading comment; must not reach the upstream)terminatorCOMMIT; INSERT INTO {table} (sm_{token}) VALUES ('sm-{token}') (transaction words then a write; must not reach the upstream)unparseableSELEC T 1 sm_{token} 'sm-{token}' (unparseable input; must not reach the upstream)unbalancedSELECT 1 AS sm_{token}, 'sm-{token} (unbalanced quote; must not reach the upstream)comment-hiddenSELECT 1 AS sm_{token} -- ; DROP TABLE sm_{token} 'sm-{token}' (near miss: hostile text inside a comment is allowed to reach)
core marks the canonical probes of the check; the others are extra coverage. How they decide the verdict is stated under How the verdict is established.
Settings and thresholds
Every check also takes checks.sql-guard-statements.enabled and checks.sql-guard-statements.severity; they are described once in the configuration reference.
This check has no numeric threshold.
| Key | Default | Standard | Meaning |
|---|---|---|---|
checks.sql-guard-statements.control_sql | none | not locked | A SELECT or WITH statement the harness sends as the control for sql-guard-statements; it must reach the upstream stub. null builds one (SELECT 1 AS sm_<token>, or a read of the first exact scope.allow entry). |
sql_tools | none | not locked | Tools that take a SQL string, as { tool, arg } (optional role: reader|writer|unknown, shape: string|array, args: { <parameter>: <string|number|boolean> } for the tool's other required parameters, which a placeholder cannot satisfy). Auto-detected when omitted. A list adds tools and corrects arguments; it never hides a detected tool (use tools.exclude for that), and a detected writer stays a writer. |
upstream.kind | none | not locked | Kind of the harness-owned upstream stub the server's backend is pointed at through {upstream}. http starts a loopback stub that records every request the server forwards (flag --upstream). null starts none. The stub needs a server Sealmark starts, and the server must use {upstream} in command, args, env, url, headers or files. |
How the verdict is established
- Scoring. critical and scored: pass = 1 point, part = 0.5, fail = 0. A fail caps the pack's grade at D, whatever the other checks scored.
What to fix
Accept a statement only when it parses as one SELECT (or WITH ... SELECT) and refuse everything else, failing closed: INSERT, UPDATE, DELETE, MERGE, CREATE, DROP, ALTER, TRUNCATE, GRANT, VACUUM, ANALYZE, SET, COPY, EXPLAIN ANALYZE of a write, stacked statements, data-modifying CTEs, transaction words, text hidden by case or comments, and anything unparseable or unbalanced. Judge the parsed statement, not its first keyword, and send nothing to the backend when you refuse. If the control statement did not reach the backend, set checks.sql-guard-statements.control_sql to one the server accepts.
False positives
A server that deliberately offers a write tool is not probed through it (a tool declared as a writer is listed as not covered). The comment-hidden near miss is allowed to reach and never counts.
False negatives
Only the statement forms in the published corpus are tried; a guard that blocks those and passes another form still passes. A server that answers from a cache, or never forwards, shows nothing at the stub.