← projects

[Execute to Public]

Postgres RPC Exposure Audit

Most dashboard functions could be called by anyone holding the public key. Measured in production, revoked, and turned into an invariant.

● Partially remediatedAuth & SecurityCompanyPostgresSQLSupabase
01 /context

The hub's data goes through Postgres functions called from the browser with a publishable key. Most are SECURITY DEFINER, so they bypass row-level security.

02 /my role

Found it in a merge review, measured it, wrote the fix and the audit query.

03 /problem

Postgres grants EXECUTE on new functions to PUBLIC. A migration that “revoked from anon” did nothing, because anon inherits from PUBLIC — and role checks existed only in the front end.

04 /how it works
  1. 01 · Measure, don't describe

    curl against production with only the publishable key: which functions answer and what they return. Some returned customer contact data.

  2. 02 · Revoke with full signatures

    A DO block over pg_proc revokes from PUBLIC and anon for every function in the schema, with exact argument lists.

  3. 03 · Verify

    401 for anonymous calls, no regression for signed-in users.

  4. 04 · Grant ≠ authorization

    Revokes decide who can call; an internal guard decides what each role sees. The FX setter, which already had a guard, became the template.

  5. 05 · Make it permanent

    The audit query (SECURITY DEFINER + executable by anon + no internal guard) became a data-health invariant.

05 /architecture
01
Merge review
02
curl probe (anon)
03
pg_proc audit
04
REVOKE PUBLIC + anon
05
Verify 401
06
Invariant
06 /interactive demo

what you're seeing

A Postgres function that returns contacts, with toggles for its grants and protections. On the right, what four callers (anonymous with the public key, member, master, owner) get back. Below, the audit query that finds exposed functions.

try this

  1. 1.Click “REVOKE FROM anon” — anonymous still gets 200. That's the PUBLIC trap.
  2. 2.Now revoke from PUBLIC and anon: anonymous is out, but the member still reads master-only rows.
  3. 3.Turn on the internal role guard: each role sees only its tier.

real vs simplified

The grant inheritance, SECURITY DEFINER bypassing RLS and the audit logic are real Postgres behavior. The function and its data are fake and masked.
[lab] loading demo…
07 /results
Margin functions fixed and verified first; the rest is being closed function family by function family — status is honestly “partially remediated”.
Secrets kept server-side; publishable and secret keys separated; API tokens moved to a secrets manager.
08 /what broke and what I changed
  • ! brokeA migration whose name said “without anon” didn't do what it said.

    → changedEvery security migration is verified with an anonymous request, not by reading it.

09 /related