Overly permissive DELETE policy
Problem
Section titled “Problem”A viewer issues DELETE FROM org_members WHERE org_id = 'X' AND user_id = '<admin>' and silently ejects an admin from an org they don’t have admin rights in.
The code
Section titled “The code”Why review misses it
Section titled “Why review misses it”The reviewer reads the SELECT and UPDATE policies (which are correctly constrained) and assumes consistency. The DELETE policy was added later “to fix a flaky test” and quietly shipped without the same constraints.
The example test that passes
Section titled “The example test that passes”Confirms the happy path. Doesn’t probe whether the policy should have stopped a wider class of deletes.
The SqlProof property
Section titled “The SqlProof property”Same idea as recipe 9: assert the post-state of a malicious write, not the return value. Wrap the operation in db.savepoint() so a policy violation doesn’t poison the outer transaction.
Note on SELECT policy interaction: In raw Postgres, a DELETE policy’s USING clause is combined with the table’s SELECT policies when filtering target rows. This means USING (true) is only exploitable when the attacker can also see the target row via the SELECT policy. The inbox schema ships a co-member visibility SELECT policy (is_member_in_org SECURITY DEFINER helper) alongside the buggy DELETE policy, making the attack observable in property tests.
The counterexample
Section titled “The counterexample”Illustrative — Hypothesis would print the actual draw and assertion:
The fix
Section titled “The fix”Add the constraints that should have shipped with the original policy. Because the admin-check subquery would query org_members from within an org_members RLS policy (causing infinite recursion), the check is routed through a SECURITY DEFINER helper:
This allows members to remove themselves (self-leave) and admins to remove any member, while blocking viewers from ejecting other members.
See also Mass assignment without WITH CHECK — the same blind spot, different operation.