Mockly

glossary

Public Table Exposure

Public table exposure happens when tables accept anon/authenticated requests without row filters or backend gating, making them effectively open. This page explains it in plain English, then goes deeper into how it works in Supabase/Postgres, what commonly goes wrong, and how to fix it without relying on fragile client-side rules.

What “Public Table Exposure” means (plain English)

If a table allows SELECT or INSERT with public credentials at all, anyone holding the anon key or an authenticated JWT can reach it.

How Public Table Exposure works in Supabase/Postgres (technical)

Exposure occurs when Postgres grants SELECT/INSERT/UPDATE/DELETE to PUBLIC/anon/authenticated while RLS is missing or too broad, so direct REST or RPC calls return rows or accept writes.

Attack paths & failure modes for Public Table Exposure

  • Public read on profiles table: A social feed component read public.profiles directly from the browser to render user cards.
  • Public write on invitations enables abuse: The frontend wrote directly to the invitations table to save teammate invites.
  • Public read on profiles table: The policy only checked that auth.uid() existed, so the authenticated role could read every row.
  • Public write on invitations enables abuse: No abuse controls existed, so attackers automated invites via the client API and overloaded the system.
  • Grants allow anon or authenticated to select or modify sensitive tables via PostgREST.
  • RLS is disabled, so row-level restrictions never run.
  • Views expose sensitive columns even if the base table is protected.
  • A public RPC function returns rows from a sensitive table (bypass via function).
  • IDs are enumerable; “unguessable” is rarely a real control.

Why Public Table Exposure matters for Supabase security

It leaks sensitive data or enables operational abuse because the database behaves like a public API endpoint with no server-side checks.

Common Public Table Exposure mistakes that lead to leaks

  • Assuming authentication alone is sufficient and granting the authenticated role full table access.
  • Believing hidden UI components mean the table is private while the DB remains accessible.
  • Running large SELECTs or writes directly from client code for convenience instead of routing through backend checks.
  • Public read on profiles table: The policy only checked that auth.uid() existed, so the authenticated role could read every row.
  • Public write on invitations enables abuse: No abuse controls existed, so attackers automated invites via the client API and overloaded the system.

Where to look for Public Table Exposure in Supabase

  • Role grants on sensitive tables and views (especially SELECT/INSERT/UPDATE/DELETE).
  • Whether RLS is enabled/forced on tables that contain user, billing, or admin data.
  • Any client-side code that queries /rest/v1/... endpoints directly.
  • RPC functions or views that indirectly expose the same data.

How to detect Public Table Exposure issues (signals + checks)

Use this as a quick checklist to validate your current state:

  • Try the same queries your frontend can run (anon/authenticated). If sensitive rows come back, you have exposure.
  • Verify RLS is enabled and (for sensitive tables) forced.
  • List policies and look for conditions that don’t bind rows to a user or tenant.
  • Audit grants to anon / authenticated on sensitive tables and functions.
  • Public read on profiles table: Authenticated doesn’t automatically mean authorized for every row.
  • Public read on profiles table: Design explicit public views instead of trusting broad table access.
  • Public read on profiles table: Backend endpoints can redact fields and apply rate limits.
  • Re-test after every migration that touches security-critical tables or functions.

How to fix Public Table Exposure (backend-only + zero-policy posture)

Mockly’s safest default is backend-only access: the browser should not query tables, call RPC, or access Storage directly.

  1. Decide which operations must remain client-side (often: none for sensitive resources).
  2. Create server endpoints (API routes or server actions) for required reads/writes.
  3. Apply hardening SQL: enable+force RLS where relevant, remove broad policies, and revoke grants from client roles.
  4. Generate signed URLs for private Storage downloads on the server only.
  5. Re-run a scan and confirm the issue disappears.
  6. Add a regression check to your release process so drift doesn’t reintroduce exposure. Fixes that worked in linked incidents:
  • Public read on profiles table: Drop the broad policy, route the request through a backend endpoint, and return a curated view tied to the requester.
  • Public write on invitations enables abuse: Move the insert to a backend endpoint with rate limiting, validation, and logging, and revoke direct client writes.

Verification checklist for Public Table Exposure

  1. Try a direct REST query as anon/authenticated and confirm whether rows return.
  2. Enable + force RLS on the table (where appropriate) and remove broad policies.
  3. Revoke table grants from client roles and verify direct access fails.
  4. Move required reads/writes to backend endpoints with explicit authorization.
  5. Validate you didn’t break the UI by implementing a secure server path first.
  6. Public read on profiles table: Authenticated doesn’t automatically mean authorized for every row.
  7. Public read on profiles table: Design explicit public views instead of trusting broad table access.
  8. Public read on profiles table: Backend endpoints can redact fields and apply rate limits.

SQL sanity checks for Public Table Exposure (optional, but high signal)

If you prefer evidence over intuition, run a small set of SQL checks after each fix.

The goal is not to memorize catalog tables — it’s to make sure the access boundary you intended is the one Postgres actually enforces:

  • Confirm RLS is enabled (and forced where appropriate) for tables tied to this term.
  • List policies and read them as plain language: who can do what, under what condition?
  • Audit grants for anon/authenticated and PUBLIC on the tables, views, and functions involved.
  • If Storage is involved: review bucket privacy and policies for listing/reads.
  • If RPC is involved: review EXECUTE grants for functions and whether privileged functions are server-only.

Pair these checks with a direct API access test using client credentials. When both agree, you can ship the fix with confidence.

Over time, keep a small “query pack” for the checks you trust and run it after every migration. That’s how you prevent quiet regressions.

Prevent Public Table Exposure drift (so it doesn’t come back)

  • Maintain an allowlist of tables that are intentionally client-readable (ideally: none for sensitive data).
  • Add a periodic audit that flags new grants to anon/authenticated on sensitive tables.
  • Use examples/templates as “known good” patterns and link them in your runbooks.
  • Keep one reusable verification test for “Public read on profiles table” and rerun it after every migration that touches this surface.
  • Keep one reusable verification test for “Public write on invitations enables abuse” and rerun it after every migration that touches this surface.

Rollout plan for Public Table Exposure fixes (without breaking production)

Many hardening changes fail because teams revoke direct access first and only later discover missing backend paths.

Use this sequence to reduce both risk and outage pressure:

  1. Implement and verify the backend endpoint or server action before permission changes.
  2. Switch clients to that backend path behind a feature flag when possible.
  3. Then revoke direct client access (broad grants, permissive policies, public bucket reads, or broad EXECUTE).
  4. Run direct-access denial tests and confirm authorized backend flows still succeed.
  5. Re-scan after deployment and again after the next migration.

This turns security fixes into repeatable rollout mechanics instead of one-off emergency changes.

Incident breakdowns for Public Table Exposure (real scenarios)

Public read on profiles table

Scenario: A social feed component read public.profiles directly from the browser to render user cards.

What failed: The policy only checked that auth.uid() existed, so the authenticated role could read every row.

What fixed it: Drop the broad policy, route the request through a backend endpoint, and return a curated view tied to the requester.

Why the fix worked: Backend code can enforce precise authorization, redact fields, and enforce rate limits before returning any profile data.

Key takeaways:

  • Authenticated doesn’t automatically mean authorized for every row.
  • Design explicit public views instead of trusting broad table access.
  • Backend endpoints can redact fields and apply rate limits.
  • Re-check policies whenever product requirements change.

Read full example: Public read on profiles table

Public write on invitations enables abuse

Scenario: The frontend wrote directly to the invitations table to save teammate invites.

What failed: No abuse controls existed, so attackers automated invites via the client API and overloaded the system.

What fixed it: Move the insert to a backend endpoint with rate limiting, validation, and logging, and revoke direct client writes.

Why the fix worked: The backend can enforce business rules, debouncing, and auditing that are hard to express safely in client policies.

Key takeaways:

  • Operational abuse is a security risk too.
  • Server endpoints can enforce backoffs and allowlists.
  • Avoid direct client writes for abuse-prone flows.
  • Audit events help detect abuse quickly.

Read full example: Public write on invitations enables abuse

Real-world examples of Public Table Exposure (and why they work)

Related terms

  • Row Level Security (RLS) → /glossary/row-level-security
  • Service Role Key → /glossary/service-role-key

FAQ

Is Public Table Exposure enough to secure my Supabase app?

It’s necessary, but not sufficient. You also need correct grants, secure Storage/RPC settings, and a backend-only access model for sensitive operations.

What’s the quickest way to reduce risk with Public Table Exposure?

Remove direct client access to sensitive resources, enable/force RLS where appropriate, and verify via a repeatable checklist that anon/authenticated cannot query what they shouldn’t.

How do I verify the fix is real (not just a UI change)?

Attempt direct API queries using the same client credentials your app ships. If the database denies access (401/403) and your backend endpoints still work, your fix is effective.

Next step

Want a quick exposure report for your own project? Run a scan in Mockly to find public tables, storage buckets, and RPC functions — then apply fixes with verification steps.

Explore related pages

parent

Glossary

/glossary

sibling

Row Level Security (RLS)

/glossary/row-level-security

sibling

Service Role Key

/glossary/service-role-key

cross

Lock down a public table (backend-only access)

/templates/access-control/lock-down-public-table

cross

Public read on profiles table

/examples/public-table-exposure/public-read-on-profiles

cross

Pricing

/pricing