Skip to main content

sql-guardrail

CI PyPI version Python versions License: MIT

A safety check for SQL that an LLM wrote.

If your app lets a language model write SQL, sql-guardrail reads each query before it runs and blocks the dangerous ones — a write when only reads are allowed, a WHERE 1=1, a missing tenant filter, a banned column, a runaway scan — with a clear reason you can log or feed back to the model.

It never touches your database, never rewrites your SQL, and never raises. You write a policy; you call verify(); you get back allowed plus a list of reasons. If it says yes, you run the exact string you gave it.

from sqlguard import Policy, verify

policy = Policy.from_yaml("policy.yml")          # load once at startup

sql = my_llm_app(question)                       # the query your model wrote
result = verify(sql, policy, context={"tenant_id": user.tenant_id})

if not result.allowed:
    raise PermissionError([v.message for v in result.violations])
conn.execute(sql)

One guardrail, not the only one. Pair it with a read-only DB role, row-level security, and a statement timeout — see Where it fits.

Why this exists

SQL injection used to be solved: you wrote the query once with placeholders and passed user input as values, so input could never change the shape of the query. Text-to-SQL breaks that — the LLM writes the whole query as text every time, so there's no fixed shape to protect and prepared statements can't help. A read-only role and row-level security catch permission errors, but the database will still run SELECT password_hash ... WHERE 1=1 and fail silently, telling you nothing. sql-guardrail is the fast first check that blocks the obvious problems with a clear reason, before the query reaches the database.

Install

uv add sql-guardrail        # or: pip install sql-guardrail

Python 3.10+. Runtime deps: sqlglot, pydantic, PyYAML, click.

The policy

A policy lists which tables and columns the LLM may touch and the rules every query must follow. A small one for a multi-tenant orders table:

# policy.yml
read_only: true                   # only reads, no writes

tables:
  - name: orders
    allow_columns: [id, product_name, account_id, total]
    require_predicate:            # every query must filter by tenant
      column: account_id
      op: "="
      value: "${tenant_id}"       # filled per request from context=

forbid:
  select_star: true               # no `SELECT *`
  always_true_predicates: true    # no `WHERE 1=1`

limits:
  max_limit_value: 1000           # no `LIMIT 1000000`

context carries per-request values that fill in ${...} placeholders. The tenant_id must come from the authenticated session, never the LLM prompt — if the LLM picks its own tenant id, every tenant rule collapses.

See examples/policy.yml for a fully-annotated policy and examples/backend_integration.py for a FastAPI-style request handler.

Validate the string you actually run. If you strip comments or normalize the SQL after verify(), call verify() again on the final form.

What it checks

  • Statement shape — SELECT only (DML when read_only=false); no multi-statement, DDL/DCL, data-modifying CTEs, locks, or SELECT INTO.
  • Always-true filters — WHERE 1=1, id = id, every constant-folded shape, across WHERE / JOIN ON / HAVING / QUALIFY / MERGE.
  • Tables & columns — per-table/column allowlists, plus a denylist that blocks a column in any position (even JOIN USING); no SELECT *.
  • Tenant isolation — require_predicate forces every alias of a table to carry your tenant filter in WHERE / INNER JOIN ON.
  • Function allowlist (opt-in) — every function call must be approved; closes pg_sleep, pg_read_file, and unvetted helpers.
  • DoS caps — joins, nesting depth, LIMIT, OFFSET, SQL length, AST size; blocks cartesian and recursive queries.

Full field reference and per-check semantics: docs/POLICY.md.

Where it fits

Safety is layers. Each catches different problems; no single one is enough.

┌──────────────────────────────────────────┐
│ 1. Prompt design + tool wrappers         │  Shape what the LLM tries.
├──────────────────────────────────────────┤
│ 2. sql-guardrail.verify()   ← THIS LIB   │  Check the SQL against a policy.
├──────────────────────────────────────────┤  (only if allowed)
│ 3. Least-privileged database user        │  Read-only, no catalog access.
├──────────────────────────────────────────┤
│ 4. Row-level security (RLS)              │  DB rule pinned to the user.
├──────────────────────────────────────────┤
│ 5. Statement timeout                     │  Slow queries get killed.
└──────────────────────────────────────────┘
                     ▼  Database

This library is layer 2: fast, in-process, no network calls. It blocks obviously bad queries with a reason you can act on. Layers 3–5 are the hard outer wall — even if a query slips past, a read-only user can't write, RLS hides other tenants' rows, and the timeout kills runaways. If this library is all you have, you've improved things but you're not safe. Set up the database layers too.

What it does NOT catch

  • Bare INSERT ... VALUES / MERGE tenant isolation — no WHERE on the target row; use DB CHECK constraints or RLS write policies.
  • Column-arithmetic tautologies (id*0=0) — safe by construction; lean on require_predicate + RLS.
  • Plan-time cost surprises — only the planner knows; use statement_timeout.
  • Bugs in your auth — if the tenant id comes from the LLM, not the session, the library can't know.

More in docs/POLICY.md.

CLI & HTTP server

Beyond the Python API, sqlguard verify validates a file, stdin, or a CI step:

echo 'SELECT id FROM orders WHERE 1=1' | sqlguard verify --policy policy.yml --stdin

Or run the HTTP sidecar — same rules, same response shape — from the image:

docker run --rm -p 8000:8000 \
  -v "$(pwd)/policy.yml:/etc/sqlguard/policy.yml:ro" \
  ghcr.io/nickusevich/sql-guardrail:latest

POST /verify returns the verdict in the body; GET /docs is the OpenAPI UI. The server does no auth — keep it internal. Exit codes, every endpoint, and env config live in docs/DEPLOYMENT.md.

Docs

  • docs/POLICY.md — every policy field and what each check does, the result object, and what the library doesn't catch.
  • docs/DEPLOYMENT.md — CLI flags and exit codes, HTTP endpoints, env-var config, Docker.
  • docs/ARCHITECTURE.md — the pipeline, module map, and the four structural defenses.
  • CONTRIBUTING.md — local checks, test layout, how to add a rule.
  • SECURITY.md — how to report a bypass privately.

The test suite is 500+ tests organized by attack class, run on Python 3.10–3.13 across Ubuntu and macOS with ruff, mypy --strict, and coverage. Every tests/malicious/*.sql must be denied and every tests/benign/*.sql allowed.

Versioning

0.5.x, SemVer. Patch bumps fix bugs (including security bypasses); minor bumps may add fields without changing existing meaning. 1.0 will commit to the public API: verify, Policy, VerificationResult, Violation, ViolationCode, ViolationCategory.

License

MIT. See LICENSE.

Metadata

Release files for sql-guardrail 0.5.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sql-guardrail 0.5.2
File Size Uploaded Provenance Actions
sql_guardrail-0.5.2.tar.gz 94.3 kB With provenance Details for sql_guardrail-0.5.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for sql-guardrail 0.5.2
File Interpreter ABI Platform Actions
sql_guardrail-0.5.2-py3-none-any.whl Python 3 none any Details for sql_guardrail-0.5.2-py3-none-any.whl

Total release size: 149.1 kB

Release files / sql_guardrail-0.5.2.tar.gz

Download URL sql_guardrail-0.5.2.tar.gz
Size 94.3 kB
Tags Source
SHA-256 checksum
How to use checksums
648233cf75d2f5076b5e6c8f6d1118f3dcc3f099834d7f910612d00031771143
BLAKE2b-256 checksum
How to use checksums
e6c452ca3d5e7ceca657b6a2bba014be27fa536b2ae5292bf63890ca760dda5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 9, 2026.

Transparency log

Release files / sql_guardrail-0.5.2-py3-none-any.whl

Download URL sql_guardrail-0.5.2-py3-none-any.whl
Size 54.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7937c03fd39ed3999dcf7a213d1840efeb682dae29a3946fd325ece7ab3c34f6
BLAKE2b-256 checksum
How to use checksums
4432c77b26f60113de10683a9e85595f51ea67b23cca00dc4c069b9c0018ff4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 9, 2026.

Transparency log

Security for sql-guardrail 0.5.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for sql_guardrail-0.5.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page