PL-004mediumLab

UPDATE policy missing WITH CHECK

Users may edit their own row, and nothing stops them changing its role, plan or owner.

Severity
Medium
Who can exploit it
Signed-in users, on rows they can already edit
Found by
Free RLS check (static)
Lab proof
Write test included for your engineer

What it is

An UPDATE policy answers two questions. USING decides which rows you may change. WITH CHECK decides what a row may look like afterwards. Leave out WITH CHECK and Postgres reuses the USING rule for the new row.

That fallback is safe for the owner column when USING is a strict ownership test. It is not enough when the row carries columns the user should never set themselves (role, plan, credits, is_verified), or when USING is broader than what a changed row should be allowed to be. Then a user edits their own row and makes themselves an admin.

Example of vulnerable code

Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.

supabase/migrations/…_profiles.sql (vulnerable)
create table public.profiles (
  id uuid primary key references auth.users (id) on delete cascade,
  full_name text,
  role text not null default 'member'
);

create policy "Users can update own profile"
  on public.profiles for update
  to authenticated
  using ((select auth.uid()) = id);

-- Any user can now run, from the browser:
--   update profiles set role = 'admin' where id = auth.uid();

Check it yourself

check-pl-004.sql
-- 1. UPDATE (and ALL) policies with no WITH CHECK
select tablename, policyname, cmd, roles, qual
from pg_policies
where schemaname = 'public'
  and cmd in ('UPDATE', 'ALL')
  and with_check is null;

-- 2. Which columns can signed-in users change?
select table_name, column_name
from information_schema.column_privileges
where table_schema = 'public'
  and grantee = 'authenticated'
  and privilege_type = 'UPDATE'
order by table_name, column_name;

The fix PatchLabs writes

PatchLabs makes the rule explicit and limits which columns a user may change. The column grant is what actually stops the role change; WITH CHECK stops a row being handed to someone else.

supabase/migrations/…_patchlabs_fix.sql
drop policy "Users can update own profile" on public.profiles;
create policy "Users can update own profile"
  on public.profiles for update
  to authenticated
  using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);
-- Users may change these columns and no others
revoke update on public.profiles from authenticated;
grant update (full_name) on public.profiles to authenticated;

Every fix arrives as a migration file (plus any code change it needs, noted in the report) for your engineer to approve. Download it, or commit it on GitHub in one click from the report page. Automatic pull requests need our GitHub App, which is coming.

The proof it runs

write test · public.profiles
[TEST]    owner set full_name    → 1 row  OK
          owner set role='admin'
            → permission denied     DENIED
          stranger set full_name
            on owner's row → 0 rows DENIED
test SQL in the reportIllustrative output · fake rows

Be clear about what is automatic. The lab's three-identity proof covers reads on every check. For write findings like this one, the report includes the test above as SQL for your engineer to run in the lab or a branch database; it is not yet part of the automatic proof.

write-test-pl-004.sql
begin;
set local role authenticated;
select set_config('request.jwt.claims',
  '{"sub":"<your user id>","role":"authenticated"}', true);
update public.profiles set role = 'admin' where id = '<your user id>';
-- before the fix: UPDATE 1 · after: permission denied for table profiles
rollback;

When the pattern is intended

A missing WITH CHECK is harmless when USING is a strict ownership test and every column on the row is one the user is allowed to set. Many tables are like that, so treat this as a prompt to look, not proof of a hole. The report says which columns signed-in users can change, so you can decide quickly.

Guides

Free check

Does your app have PL-004?

Paste your public repo. We rebuild the database from your migrations, test it as a visitor, a stranger and the owner, and hand you the proven fix.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.