Skip to content
VibeSafeDaily

Exposed data

A database function returns rows the table policy hides

Cataloged October 17, 2026 · Lovable · Bolt · Cursor · Replit · v0 · Supabase

Symptom

Row level security is on, and a signed-out read of the table is empty. A database function still returns other people's rows. The dashboard can show that function as not exposed.

Root cause

Supabase's database functions guide says to prefer security invoker, which is the default. A security definer function runs with its creator's privileges. A function created in the Dashboard or by a migration is owned by postgres, which bypasses row level security. Any role can call it by default, including anon, the role that serves a request with no user session. The guide says an execute grant does not limit the rows a permitted role gets back, so granting execute to authenticated still returns any user's row to every signed-in caller, and that pinning search_path does not change who can call the function. The row level security guide says a security definer function in an exposed schema is callable over the Data API with the creator's privileges, and says never to create one in a schema listed under Exposed schemas. On Supabase that owner is postgres, which has bypassrls. Advisor lint 0028 flags a security definer function callable without signing in. The page says anyone with the public anon key can call it through POST /rest/v1/rpc/ and can read or modify data that row level security would block, and that /rest/v1/rpc accepts any function name the role can execute, whether or not the app calls it. Postgres grants execute on a new function to PUBLIC, and Supabase also grants execute to anon, authenticated, and service_role. Lint 0029 is the same finding for signed-in users. The page says revoking from anon alone usually leaves the function callable by every signed-up user. The Database Functions guide and the Securing your API guide still show a schema-scoped revoke, alter default privileges in schema public revoke execute on functions from public. GitHub issue 49338, opened August 20, 2026, says that statement does not remove the built-in PUBLIC grant. The author writes that a security definer function reading auth.users stayed publicly callable on a production project after that hardening. A comment on October 1, 2026 confirmed the same on a hosted project: after the two schema-scoped statements from the Database Functions guide, has_function_privilege for anon was still true. The schema-less form, alter default privileges for role postgres revoke execute on functions from public, made that probe return false. The comment says that form applies to every later function that role creates, in any schema, including functions added by an extension. Issue 50441, opened September 16, 2026, says Studio can list a function as revoked under Exposed functions while has_function_privilege for anon is true and a signed-out POST to /rest/v1/rpc/ returns HTTP 200 with current_user anon. The author says no production database was used. After revoke execute on that function from public, anon, and authenticated, the same call returned HTTP 401 and 42501, permission denied for the function.

Before and after

The after line is the revoke from issue 50441. Put the real identity arguments inside the parentheses. A function the app must keep calling still needs security invoker, or an auth.uid() check plus a grant to authenticated.

Before

-- anon can execute, including through the PUBLIC grant

After

revoke execute on function public.your_function()
  from public, anon, authenticated;

Fix prompt

In this project's database only. Do not connect to any other Supabase project, and do not call an rpc on a project you do not own.

1. In the SQL editor, list security definer functions in every schema under API settings, Exposed schemas. For each, record the name, the identity arguments from pg_get_function_identity_arguments, the owner, and has_function_privilege for anon and for authenticated. Ignore the Exposed functions badge until it matches has_function_privilege.

2. If a policy calls the function, move that function into a schema that is not listed under Exposed schemas. Follow the private schema example in Supabase's row level security guide: security definer, search_path = '', every name schema-qualified, and a filter on (select auth.uid()) when the function reads per-user rows. Revoke execute from public. Grant usage on that schema, and execute on the function, only to the roles that policy is for. Point the existing policy at the new name with alter policy. Do not drop the policy and recreate it from an example.

3. If the client calls the function with rpc() and the function does not need to bypass row level security, switch it to security invoker:
   alter function public.your_function(arg_types) security invoker;
   Replace your_function and arg_types with the identity arguments from step 1.

4. If the client calls it and it must stay security definer, check (select auth.uid()) inside the body and ignore any user id passed as an argument. Then revoke execute from public and anon, and grant execute only to authenticated.

5. If only server code should call it, revoke execute from public, anon, and authenticated in one statement. Include the argument types. service_role keeps its own grant. Do not grant execute to anon or authenticated.

6. In the migration that creates any new function, add that same revoke. Do not stop after alter default privileges in schema public revoke execute on functions from public. Issue 49338 shows that statement leaves a new function executable by anon. Do not run the schema-less alter default privileges for role postgres across every schema. The October 1, 2026 comment says that form also covers functions an extension creates.

Do not print row contents. Do not edit unrelated files.

Paste this into the coding tool that has your own project open.

2-minute self-check

These steps are for an app you own. Do not run them against someone else's project.

  1. In your own SQL editor, list security definer functions in the public schema and select has_function_privilege for anon and for authenticated, using each function's identity arguments. A true value for anon on a function that reads private rows is the failure.
  2. In your own dashboard, open the security advisor and look for lint 0028, callable without signing in, and lint 0029, callable by signed-in users. If Exposed functions says revoked, run has_function_privilege anyway. Issue 50441 shows those two answers can disagree.
  3. Signed out of your own app, use your own project URL and publishable key and call one rpc your own app uses. The response should not include another user's rows. A function that is not part of the product should be denied. Do not point this call at any other project.

Common questions

Row level security is on and a signed-out select is empty. Why does a database function still return every row?

A security definer function runs as its owner. Supabase's database functions guide says a function created in the Dashboard or by a migration is owned by postgres, which bypasses row level security, and that anon can call it by default. Row policies do not apply to the function. An execute grant does not filter rows, so granting execute to authenticated still returns any user's row to every signed-in caller. Advisor lint 0028 is the signed-out finding, and lint 0029 is the signed-in one.

I revoked execute from anon, and Exposed functions says revoked. Is the function closed?

Check has_function_privilege for anon, including the function's argument types. Issue 50441 says Studio can report revoked while that privilege is still true, because execute granted to PUBLIC does not show up as a grant to anon. Issue 49338 says the schema-scoped statement alter default privileges in schema public revoke execute on functions from public leaves a new function executable by anon. Revoke from public, anon, and authenticated on that function. On the 50441 report, that revoke changed the signed-out call from HTTP 200 to HTTP 401 with 42501, permission denied for the function. The author of that issue says no production database was used.

Sources

Same symptom

Same platform