# Sensitive columns are not readable through the API on publicly readable tables

- **Pattern:** `ab-002642` (`supabase-rls-keys.rls-coverage.sensitive-columns-not-api-readable`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-10-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002642
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002642)

## Why it matters

RLS filters rows, never columns. When a table is readable by `anon` or by every signed-in user (a waived `USING (true)` policy, or any read policy that is not tied to the caller), every column of every row comes back from the REST endpoint with a plain `select=*`. Supabase also grants table-level privileges on new `public` tables to `anon` and `authenticated` by default, so the only things that hide a column are a column-level grant, a separate owner-only table, or a non-exposed schema. AI-built apps routinely put a `password_hash`, `api_key`, `stripe_customer_id`, `phone`, `date_of_birth`, or `email` column on the same `profiles` or `users` table they marked public for the leaderboard, so one anonymous GET dumps every user's secrets and personal data. The Supabase database advisor flags this class as lint 0023, sensitive columns exposed. This check judges what anon and authenticated can actually read after column grants and views are taken into account.

## Severity rationale

Critical because a sensitive column on a publicly readable table is exfiltrated with one unauthenticated GET, exposing credentials or personal data for every row at once.

## Remediation

Take the sensitive column out of what the API role can read. The cleanest fix is a separate table: keep public fields in `public.profiles` and move secrets to `public.profile_secrets` with owner-only RLS (or to a schema that is not exposed). To keep one table, revoke the table-level grant and grant only the safe columns:

```sql
revoke select on public.profiles from anon, authenticated;
grant select (id, username, avatar_url) on public.profiles to anon, authenticated;
```

Column privileges only take effect once the table-level grant is revoked, and clients must then name their columns (`select=id,username`) because `select=*` fails with permission denied. Do not hide a column behind a view unless the view omits it AND the base table is no longer readable by `anon` and `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 0023_sensitive_columns_exposed — https://supabase.com/docs/guides/database/database-advisors?lint=0023_sensitive_columns_exposed

Taxons: access-control, privacy-consent

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