# No view in an exposed schema exposes auth.users data to anon or authenticated

- **Pattern:** `ab-002645` (`supabase-rls-keys.rls-bypass-surfaces.auth-users-not-exposed-via-view`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-10-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002645
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002645)

## Why it matters

The `auth` schema holds every user's email, phone, password hash, confirmation and recovery tokens, and raw metadata, and it is deliberately not exposed through the API. A developer who wants a 'users list' or to join profiles to emails writes `create view public.users as select id, email, raw_user_meta_data from auth.users`. If that view runs as its owner (the default) and `anon` or `authenticated` can read it, one API call returns every user's email address and metadata to anyone with the public key, and the view bypasses the lock on `auth.users` entirely. Even with `security_invoker` the pattern is wrong because the caller has no rights on `auth.users` and the view either errors or gets 'fixed' by loosening grants. AI tools reach for this shortcut constantly because the dashboard shows `auth.users` as the place users live. The advisor flags it as lint 0002.

## Severity rationale

Critical because one unauthenticated request can return every registered user's email and metadata, turning the identity table into a public contact list for phishing and account takeover.

## Remediation

Do not read `auth.users` from the API. Copy the few fields the app needs into your own table with a trigger and protect that table with RLS:

```sql
create table public.profiles (
  id uuid primary key references auth.users on delete cascade,
  display_name text
);
alter table public.profiles enable row level security;

create function private.handle_new_user() returns trigger
language plpgsql security definer set search_path = '' as $$
begin
  insert into public.profiles (id, display_name)
  values (new.id, new.raw_user_meta_data ->> 'display_name');
  return new;
end $$;
```

If a view over `auth` already exists, drop it or move it to a non-exposed schema, and `revoke all on <view> from anon, authenticated`.

## 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-200
- owasp A01
- external 0002_auth_users_exposed — https://supabase.com/docs/guides/database/database-advisors?lint=0002_auth_users_exposed

Taxons: access-control, privacy-consent

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