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.

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.