re module, the engine that evaluates guardrails, using both queries that must be blocked and legitimate queries that must pass.
For rule creation, actions, and pattern syntax basics, see Guardrails Configuration.
Two pitfalls that break most SQL rules
Real queries span multiple lines. Real scripts contain more than one statement. These two regex details cause most false positives and false negatives: 1.. does not cross line breaks. Without the s flag, a lookahead like (?!.*\bWHERE\b) only inspects the first line. This rule blocks a multi-line UPDATE even though it has a WHERE clause:
[^;]* instead of .*. It crosses line breaks and stops at the statement boundary.
2. ^ only matches the start of the payload. Without the m flag, a rule anchored on ^UPDATE misses the UPDATE in SELECT 1; UPDATE t SET a = 1;. Prefer \b word boundaries plus per-statement scoping over anchors.
The recipes below apply both fixes. They share a common shape: (?is) at the start (case-insensitive, dot crosses lines) and [^;]* to stay inside one statement.
Recipes
Each recipe is one input rule with the Block action, unless noted. Rule names follow theblock-* convention; use them as-is or rename to match your setup.
block-tautology-where
Blocks statements where the WHERE clause exists but does not restrict anything:WHERE 1=1, WHERE TRUE, WHERE '1'='1'.
The pattern only matches when the tautology is the last or only condition, followed by
;, end of input, or RETURNING. That keeps the common query-builder idiom WHERE 1=1 AND <real condition> working. To block that idiom too, remove the ;|$ alternatives.
block-write-with-subquery
Blocks UPDATE and DELETE statements whose scope comes from a subquery:IN (SELECT ...), EXISTS (SELECT ...), = (SELECT ...). These writes are hard to review because the affected rows are invisible until execution.
Reads with subqueries stay allowed. Column names like
updated_at do not trigger the rule: \b requires the exact word.
block-cte-write
Blocks writes hidden behind a CTE:WITH ... UPDATE, WITH ... DELETE, WITH ... INSERT, WITH ... MERGE. A rule that only inspects the first keyword of a query sees WITH and lets the write through.
Read-only CTEs pass. The rule requires
WITH at the start of a statement, so table hints like WITH (NOLOCK) in the middle of a query do not trigger it.
block-excessive-joins
Matches statements with 3 or more JOINs. Regex cannot parse query structure, but it can count occurrences. Adjust{2} to N-1 for a different threshold.
block-write-without-where
Blocks UPDATE and DELETE statements that have no WHERE clause at all. This is the corrected, multi-line-safe version of the classic rule.
This rule and
block-tautology-where are complementary and never overlap: UPDATE ... WHERE 1=1; triggers the tautology rule, not this one. Assign both to the same resource role for full coverage.
Test before you deploy
Never test a blocking rule with a query that would be destructive if the block fails. A typo in the pattern, or a guardrail assigned to the wrong resource role, means the query runs. The test queries below are safe on any database, including production:- “Must block” queries never reach the database when the rule works. They also reference tables that do not exist (
gr_test_zz,gr_tmp_zz,gr_orders_zz), so even if the rule fails, Postgres returnsrelation does not existand nothing changes. For an extra layer, wrap writes inBEGIN; ...; ROLLBACK;(neither keyword triggers these rules). - “Must pass” queries execute for real. They only read Postgres catalogs (
pg_tables,pg_class) with aLIMIT, and the single write targets a temp table created in the same payload, discarded when the session ends. On a read-only resource role, the temp table control fails on permissions, which is harmless.
- An
UPDATE ... WHERE id = 1split across several lines must passblock-write-without-where. Multi-line queries are the main source of false positives in older rule styles. UPDATE ... WHERE 1 = 1;must triggerblock-tautology-where, notblock-write-without-where.
What regex cannot catch
ALIMIT that only exists in a subquery is not detectable with regex. Consider:
- Deciding whether the
LIMITbelongs to the outer query or the subquery requires parsing nested parentheses. That is grammar, and regex does not parse structure. - A “SELECT without LIMIT” rule does not help either: the word
LIMITis present in the text, so any textual check considers the query bounded.
SET note = 'where true' can trip a tautology rule, and /* where */ can satisfy an absence check. Rare in practice, but real. A guardrail is a textual safety belt, not a SQL parser.
For unbounded reads, layer your defenses instead:
- Require Approval: route mass-read verbs on production resource roles through approval. The approver sees the query structure that regex cannot.
- Database-level controls: give the resource role’s database user a
statement_timeout, a read-only role, or views with built-in limits on sensitive tables. The guardrail blocks the text; the database limits the actual cost. - Runbooks: parameterized queries for day-to-day operations. Users fill in parameters instead of writing free-form SQL.
Related
Guardrails Configuration
Rule creation, actions, pattern syntax, and troubleshooting
Guardrails Overview
What guardrails do and how they fit with other features
Action Access Requests
Configure the Require Approval action
Runbooks
Parameterized queries instead of free-form SQL