pgrls
A static analyzer for Postgres Row-Level Security. Connects to a live database, runs 44 rules over every policy, and flags the semantic bugs (broken row scoping, inverted auth checks, missing WITH CHECK, BYPASSRLS roles, view-mediated bypasses) that eyeball review misses. 12 of the 44 rules mechanically auto-fix. MIT, Python 3.11+, tested PostgreSQL 15–17.
pip install pgrls
export DATABASE_URL='postgres://…'
pgrls lint
New in 0.6.1 — SEC033
A Supabase RLS policy that gates on user_metadata is self-bypassable in one line of client code:
USING (auth.jwt() -> 'user_metadata' ->> 'role' = 'admin')
-- exploit: await supabase.auth.updateUser({ data: { role: "admin" } })
user_metadata is end-user writable via the standard Supabase auth API — by design. The safe counterpart is app_metadata (service-role-only). pgrls lint --rule SEC033 catches every shape (all four JSON operators + raw_user_meta_data column refs). Default severity error — fails CI on first sight. Released 2026-05-24.
Quick links
- Quickstart — 5 minutes from
pip installto a real RLS finding. - Rule reference (AGENTS.md) — all 44 rules, each with detection logic, severity, and remediation.
- GitHub Action on the Marketplace — one-line CI integration.
- CHANGELOG.
On this site
- Comparisons — pgrls vs adjacent tools and how it fits with Postgres ecosystems (Supabase, PostgREST, Hasura, Django).
The other bug pgrls catches (the classic)
CREATE POLICY tenant_read ON public.documents
FOR SELECT
USING (auth.uid() IS NULL OR owner_id = auth.uid());
Reads correct in English; ships past code review; admits every row to unauthenticated clients because auth.uid() returns NULL for any session without a JWT, the IS NULL branch is true, the OR short-circuits. pgrls flags it as SEC004 in milliseconds. 42 other rules cover the rest of the RLS bug space.