Insert and update policies bind row ownership in WITH CHECK
Why it matters
A Supabase RLS policy has two halves. USING decides which existing rows you may see or touch; WITH CHECK decides what a row may look like after you write it. Row-level security can be enabled on every table and still let any signed-in user write anything, because an INSERT policy with no WITH CHECK, or one whose check is only auth.role() = 'authenticated', never ties the new row to the caller. The client picks the user_id (or tenant_id) in its own request body, so a logged-in attacker inserts rows owned by someone else, plants records in another tenant, or updates their own row to reassign it to a victim. AI-generated migrations produce this constantly: the model writes a tidy FOR SELECT USING (auth.uid() = user_id) policy, then adds FOR INSERT TO authenticated WITH CHECK (true) or omits the check because the insert 'just works' in the demo. This check covers ownership binding in write policies; policies whose predicate is literally true are judged by the Stack Scan RLS check and are deliberately not re-judged here.
Severity rationale
High because a write policy that does not bind the written row to the caller lets any authenticated user forge or reassign rows across tenants, which is a direct broken-access-control path, but it requires a valid session and does not by itself expose reads.
Remediation
Give every INSERT, UPDATE, and FOR ALL policy an explicit WITH CHECK that compares a column of the written row to the caller. Wrap auth.uid() in a subselect so Postgres evaluates it once per statement:
create policy "owner can insert" on public.todos
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "owner can update" on public.todos
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
For tenant tables, check membership instead: with check (exists (select 1 from memberships m where m.tenant_id = todos.tenant_id and m.user_id = (select auth.uid()))). Also fill user_id from a column default of auth.uid() so the client does not choose it at all.
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the Supabase RLS & Keys audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- cwe:4.14 · CWE-284 — Improper Access Control
- cwe:4.14 · CWE-285 — Improper Authorization
- cwe:4.14 · CWE-639 — Authorization Bypass Through User-Controlled Key
- owasp:2021 · A01 — Broken Access Control
- external · 0024_permissive_rls_policy — Supabase database advisor lint 0024 permissive RLS policy (related)
Taxons
History
- 2026-10-03·v1.0.0·Initial supabase-rls-keys v1 authoring; write-side RLS policy ownership binding in WITH CHECK, split out from the Stack Scan RLS check which owns literal true predicates.·by supabase-rls-keys-v1-build