# RLS policies do not authorize on user-editable user_metadata

- **Pattern:** `ab-002647` (`supabase-rls-keys.policy-trust-inputs.policies-do-not-trust-user-metadata`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-10-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002647
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002647)

## Why it matters

Supabase Auth stores two metadata blobs in the user's JWT. `app_metadata` can only be written with the service role. `user_metadata` (also `raw_user_meta_data` in the `auth.users` table) is writable by the user themselves through `supabase.auth.updateUser({ data: { role: 'admin' } })`. A policy such as `using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin')` therefore lets any signed-in user make themselves an admin with one client call, then read and write everything the policy guards, and the same applies to tenant ids and plan flags. This is the classic AI-generated role check: the model sees a role claim in the token and picks the first metadata key it finds. The advisor flags it as lint 0015. Using `app_metadata` (set only server-side), a roles table keyed by `auth.uid()`, or Supabase custom claims via an auth hook is safe.

## Severity rationale

Critical because the attacker needs only a normal account and one client call to forge the claim the policy trusts, granting admin or cross-tenant access to everything the policy protects.

## Remediation

Authorize on data the user cannot edit. Put roles and tenant ids in `app_metadata` (set server-side with the service role or an auth hook), or in a table the user cannot write, and read them in policies:

```sql
create policy "admins read all" on public.orders
  for select to authenticated
  using ((select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin');
```

Replace every `user_metadata` and `raw_user_meta_data` reference in a policy or in a function a policy calls. Treat any role previously granted this way as suspect and review who holds it.

## Detection

**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](https://auditbuffet.com/audits/supabase-rls-keys). The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.

## External references

- cwe CWE-284
- cwe CWE-285
- cwe CWE-639
- owasp A01
- external 0015_rls_references_user_metadata — https://supabase.com/docs/guides/database/database-advisors?lint=0015_rls_references_user_metadata

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002647
