sql-guard-statements

SQL guard: only read statements reach the backend

shipped

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-guard opt-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

  • control SELECT * 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 core
  • insert INSERT INTO {table} (sm_{token}) VALUES ('sm-{token}') (writes; must not reach the upstream)
  • update UPDATE {table} SET sm_{token} = 'sm-{token}' WHERE 1 = 0 (writes; must not reach the upstream)
  • delete DELETE FROM {table} WHERE sm_{token} = 'sm-{token}' (writes; must not reach the upstream)
  • merge MERGE 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)
  • create CREATE TABLE sm_{token} (v text DEFAULT 'sm-{token}') (DDL; must not reach the upstream)
  • drop DROP TABLE IF EXISTS sm_{token} /* sm-{token} */ (DDL; must not reach the upstream)
  • alter ALTER TABLE {table} ADD COLUMN sm_{token} text DEFAULT 'sm-{token}' (DDL; must not reach the upstream)
  • truncate TRUNCATE TABLE {table} /* sm_{token} sm-{token} */ (DDL; must not reach the upstream)
  • grant GRANT SELECT ON {table} TO sm_{token} /* sm-{token} */ (privileges; must not reach the upstream)
  • vacuum VACUUM {table} /* sm_{token} sm-{token} */ (maintenance; must not reach the upstream)
  • analyze ANALYZE {table} /* sm_{token} sm-{token} */ (maintenance; must not reach the upstream)
  • set-session SET SESSION sm_{token} = 'sm-{token}'; SELECT 1 (session state; must not reach the upstream)
  • copy COPY {table} (sm_{token}) TO '/tmp/sm-{token}' (file access; must not reach the upstream)
  • stacked SELECT 'sm-{token}'; DROP TABLE sm_{token} (multiple statements; must not reach the upstream)
  • cte-dml WITH 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-write EXPLAIN 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)
  • terminator COMMIT; INSERT INTO {table} (sm_{token}) VALUES ('sm-{token}') (transaction words then a write; must not reach the upstream)
  • unparseable SELEC T 1 sm_{token} 'sm-{token}' (unparseable input; must not reach the upstream)
  • unbalanced SELECT 1 AS sm_{token}, 'sm-{token} (unbalanced quote; must not reach the upstream)
  • comment-hidden SELECT 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.

KeyDefaultStandardMeaning
checks.sql-guard-statements.control_sqlnonenot lockedA 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_toolsnonenot lockedTools 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.kindnonenot lockedKind 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.