Example finding How it works Coverage PENTEST METHODOLOGY DOCS PRICING FAQ MCP CONTACT LOG IN SIGN UP →

Supabase RPC function callable by anyone: how to fix it

FIX / OPEN RPC FUNCTION / 9 MIN READ

Any role can run a new Postgres function by default, so revoke EXECUTE on the function from PUBLIC and anon, grant it only to the roles that need it, and check the user inside the function with auth.uid(). Supabase exposes your functions through the rpc endpoint of the API. A visitor who has the public key can call them. This guide shows how to find these functions, close them, and check what they already did.

What does “RPC function callable by anyone” mean?

It means that a visitor can run a database function in your project without a sign-in. The visitor only needs the public anon key. This key is in your frontend code, so it is not a secret. See what the Supabase anon key can do for more on this.

Supabase lets your app run functions with the rpc() method of the client. The Supabase database functions documentation gives this example: supabase.rpc('hello_world'). The client sends an HTTP request to POST /rest/v1/rpc/<function name>. An attacker can send the same request with curl.

The cause is a default in Postgres. The documentation states: “By default, any role can run a database function.” The Securing your API guide says the same: “New functions are callable by every role.”

RLS does not protect functions. The same guide states that “RLS doesn’t apply to functions, so grant execute only to the roles that need to call them.” RLS controls access to rows in a table. The right to call a function is a separate right.

Two properties of a function decide the size of the risk:

Property Meaning Risk
security invoker (default) The function runs with the rights of the caller Lower. RLS still applies to the tables that the function reads
security definer The function runs with the rights of its owner Higher. The owner is often postgres, which bypasses RLS

The Supabase documentation warns that “a security definer function can return rows the caller isn’t allowed to read.” It also says that functions created in the dashboard or by a migration are owned by postgres.

Vibe coding tools often create this problem. You ask for a feature such as “give a user a credit” or “delete my account”. The tool writes a security definer function so that the feature works around RLS errors. The function accepts a user ID as a parameter. The grant is never changed, so any visitor can call it with any user ID.

How do I confirm that a function is callable by anyone?

Use a list query and a real request. The query shows the grants. The request shows the real behavior.

List the functions and their grants

Run this query in the SQL editor:

select
  n.nspname as schema,
  p.proname as name,
  pg_get_function_identity_arguments(p.oid) as args,
  p.prosecdef as security_definer,
  has_function_privilege('anon', p.oid, 'execute') as anon_can_run,
  has_function_privilege('authenticated', p.oid, 'execute') as authenticated_can_run
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prokind = 'f'
order by anon_can_run desc, security_definer desc, p.proname;

Read the result from the top. Rows with anon_can_run = true are open to every visitor. Rows with security_definer = true run with the rights of the owner. A row with both values is the worst case.

The query lists the public schema. If your project exposes other schemas in the API settings, change the nspname filter to include them. Some functions in the list come from extensions. Ignore these if you did not create them, and check them later.

Call the function as an attacker

Replace the values and run this command from your terminal:

curl -s -X POST "https://YOUR-PROJECT.supabase.co/rest/v1/rpc/FUNCTION_NAME" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

If the function needs parameters, add them in the JSON body. A 403 or a 401 response with a permission error means the function is closed to anon. A 200 response, or an error that comes from inside the function, means the call reached the function. Both results show that the function is open.

Warning: Do not test a function that deletes, updates, or sends data with real user IDs. Use a test row or read the function code first.

Use the Security Advisor

The Supabase Security Advisor has two checks for this problem. The database advisors documentation lists them:

  • 0028_anon_security_definer_function_executable: “This SECURITY DEFINER function is callable without signing in.”
  • 0029_authenticated_security_definer_function_executable: “This SECURITY DEFINER function is callable by signed-in users.”

The Advisor checks only security definer functions. A security invoker function that anyone can call does not appear. Use the list query above to find these.

How do I close the function now?

Do the steps in this order. Steps 1 and 2 close the function for visitors.

Caution: The revoke command stops every call that uses the revoked role. If your app calls the function before sign-in, the feature breaks. Check the code that calls the function before you revoke.

  1. Revoke EXECUTE from PUBLIC and anon. Do this for each open function.
  2. Grant EXECUTE to the roles that need it. Most functions need only authenticated.
  3. Change security definer to security invoker if you can. The function then follows RLS.
  4. Check the user inside the function. Use auth.uid() and do not trust a user ID from a parameter.
  5. Set the search_path of each remaining security definer function.
  6. Revoke the default for new functions. Then the problem does not come back.
  7. Run the test again. Use the curl command and the list query.

This is the revoke for step 1. The documentation gives the same form:

revoke execute on function public.give_credit(uuid, int) from public;
revoke execute on function public.give_credit(uuid, int) from anon;
grant execute on function public.give_credit(uuid, int) to authenticated;

You must revoke from PUBLIC as well as from anon. Postgres gives the default grant to PUBLIC, and anon is a member of this group. The Advisor documentation for check 0029 states this: revoke from PUBLIC too, “because Postgres’ default grant applies there.” If you revoke only from anon, the function can stay callable.

The function signature in the command must include the argument types. If you use a wrong signature, Postgres returns an error that the function does not exist.

To close all functions in the schema at one time, use the schema-wide form from the documentation:

revoke execute on all functions in schema public from public;
revoke execute on all functions in schema public from anon, authenticated;

Warning: This command removes the rights from all functions in the schema, including the helper functions that your RLS policies use. The Supabase guide says that policies which use a revoked function can fail. Grant EXECUTE again on each function that your app or your policies need.

How do I write a safe function?

A safe function checks who the caller is and does not trust the parameters. This is an unsafe example:

create function public.give_credit(target_user uuid, amount int)
returns void
language sql
security definer
as $$
  update public.profiles set credits = credits + amount where id = target_user;
$$;

Any caller can give any amount to any user. This is a safer version:

create or replace function public.claim_daily_credit()
returns void
language plpgsql
security invoker
set search_path = ''
as $$
begin
  update public.profiles
  set credits = credits + 1, last_claim = now()
  where id = (select auth.uid())
    and (last_claim is null or last_claim < now() - interval '1 day');
end;
$$;

revoke execute on function public.claim_daily_credit() from public, anon;
grant execute on function public.claim_daily_credit() to authenticated;

The function takes no user ID and no amount. The server decides both values. The function uses security invoker, so the RLS policy on profiles also applies. Note that this needs an update policy for the owner on profiles. If you add a column such as credits, protect it with a policy or a trigger, so that users cannot change it with a direct update.

If a function must use security definer, for example to write to a table that users cannot write to, apply these rules:

  1. Set search_path to an empty value, as the Supabase documentation requires for security definer functions: set search_path = ''.
  2. Use full names for all tables, such as public.profiles.
  3. Take the user from auth.uid() and never from a parameter.
  4. Validate each parameter and reject values outside the allowed range.
  5. Return only the columns that the caller may read.
  6. Keep the function in a schema that is not exposed, if the API does not need to call it.

How do I stop new functions from being open?

Change the default privileges. The documentation gives these two commands:

alter default privileges revoke execute on functions from public;
alter default privileges in schema public revoke execute on functions from anon, authenticated;

The first command has no in schema part. The documentation explains that a revoke limited to one schema has no effect on the global default. The first command also changes functions that postgres creates in other schemas. Grant EXECUTE on these helper functions to the roles that call them.

After this change, each new function is closed until you grant EXECUTE. This is the safe default. Add the grant line to each migration that creates a function.

Which functions can I keep open on purpose?

Some functions must work without a sign-in. Examples are a check of a username, or a public product list. Keep these functions open only when all of these rules are true:

  • The function uses security invoker.
  • The function returns only data that every visitor may read.
  • The function does not write data, or the write is harmless and limited.
  • The function checks all input.
  • The function has a rate limit, for example in your API layer.

Use a view or a table with an RLS policy if you can. A table with a correct policy is easier to review than a function. For rate limits, see API abuse protection.

Authenticated users are not always safe users. If sign-up is open, anyone can create an account in a few seconds and become authenticated. Do the same checks inside the function for these callers, and do not rely on the role alone.

How do I check what happened before I closed the function?

Check the logs for calls to the function and check the data that the function can change. Use the order below.

  1. Read the function code. Write down what it reads and what it writes. This tells you what to look for.
  2. Search the API logs. In the Supabase dashboard, open Logs and select the API Gateway source. Search for the path /rest/v1/rpc/ and the name of your function. The logs documentation lists the log sources. The retention depends on your plan.
  3. Look for calls with only the public key. Calls without a user token are anonymous. Look for IP addresses and user agents that your app does not use, such as curl.
  4. Check the Postgres logs. Look for errors from your function. A burst of errors can show that somebody tried values.
  5. Check the data. Look for rows that changed in a way that your app cannot cause. Look at credits, roles, balances, and flags such as is_admin.
  6. Check for the exposure of data. If the function returns rows, assume that an attacker read them. List the columns that it returns.

If the function changed money, credits, or roles, restore the correct values from a backup. If the function returned personal data, you may need to tell the users. If an attacker could change a role, read privilege escalation in SaaS apps for the next steps.

If you cannot find the calls in the logs, the retention of your plan may be too short. Assume the worst case in that situation. The function was open from the day you created it.

How do I prevent open functions in the future?

Use these controls.

  1. Set the default privileges once. Run the alter default privileges commands from this guide.
  2. Add the grant to each function migration. A function without a grant must fail the review.
  3. Prefer security invoker. Use security definer only with a written reason.
  4. Use a separate schema for the API. The Securing your API guide says that a dedicated schema makes “the exposed surface easier to identify and audit.” Keep helper functions in a schema that the API does not expose.
  5. Run the Security Advisor before each release. Fix the 0028 and 0029 findings at once.
  6. Add a check to your tests. Run the list query in the test suite and fail the build if an unexpected function is open to anon.
  7. Tell your coding tool the rule. Add “Never create a security definer function without a grant review and an auth.uid() check” to the project instructions.
  8. Scan the live app. A scan calls the RPC endpoints with the public key, as an attacker does.

For the row access rules around these functions, read the Supabase RLS guide.

When should I get help?

Get help in these cases:

  • The function writes to payment, balance, or role data, and the logs show calls that you cannot explain.
  • The function returned personal data and you cannot say which users were affected.
  • The project has many functions and you cannot review each one. A security engineer can review them in a short time.
  • Your RLS policies call functions and break after you revoke EXECUTE. Grant the rights to the helper functions again, one at a time, and test after each change.
  • You think an attacker used a security definer function to read or change data in a table that RLS protects.

After you finish, run the list query again. No function that you did not choose to open may show anon_can_run = true.

Common questions

Why can anyone call my Supabase function?
Postgres grants EXECUTE on new functions to all roles by default. Supabase exposes functions in the API schemas through the rpc endpoint. The public key is enough to call them.
Does RLS protect my function?
No. RLS does not apply to the right to call a function. A security definer function also runs as its owner and can bypass RLS inside the function.
Is it enough to revoke EXECUTE from anon?
No. The default grant is to the PUBLIC pseudo-role, and anon is a member of it. Revoke from PUBLIC and from anon, then grant EXECUTE only to the roles that need it.
Is a function that only authenticated users can call safe?
Not always. Anyone can sign up and become authenticated if sign-up is open. Check the user inside the function with auth.uid().
Can I keep a public function on purpose?
Yes. Use a security invoker function that returns only data that every visitor may read. Validate all input and keep the function simple.

Find open rpc functions

A VibeEval scan calls the RPC endpoints of your deployed Supabase app with only the public key and reports functions that run without a sign-in.

14-day free trial · No credit card · Cancel anytime

SCAN YOUR APP →