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

Supabase warning: function search_path mutable

FIX / SEARCH PATH MUTABLE / 8 MIN READ

The warning means that a function in your database has no fixed search_path, so Postgres looks up table and function names in schemas that can change at run time. Fix it with alter function schema.name(args) set search_path = ''; and add the schema name to each table and function inside the body. The risk is highest for functions that use security definer. This guide shows how to find each function, fix it, and test that the function still works.

What does “function search_path mutable” mean?

The warning comes from the Supabase database linter. It is lint 0011_function_search_path_mutable. The linter documentation gives it the level WARN and the title “Unsecured function search path”.

The search_path is a Postgres setting. It is a list of schemas. When a query uses a name without a schema, such as profiles, Postgres looks for that name in each schema of the list, in order. The first match wins.

A function that has no fixed search_path uses the setting of the session that calls it. The caller can change this setting. The linter documentation states that such a function could behave unpredictably or could be exploited to reference unintended database objects.

The Postgres documentation for CREATE FUNCTION explains the attack. A function must not search schemas that untrusted users can write to. The temporary schema is a special case. Postgres searches it first by default, and every user can write to it. A user can create a temporary table with the name that the function expects. The function then reads the wrong table.

The risk is larger for security definer functions. These functions run with the privileges of the function owner. An attacker who masks a name can make the function run the attacker’s code with the owner privileges. The Supabase functions documentation states that you must set the search_path when you use security definer.

AI coding tools create these functions often. A common case is a trigger function that creates a profiles row when a user signs up. The tool writes create function public.handle_new_user() ... security definer and does not set the search_path. The function works, so the missing line stays unnoticed until the linter reports it.

How do I find the functions that cause the warning?

Use the Security Advisor first. Then use SQL to get a full list.

  1. Open the Supabase dashboard.
  2. Open the Advisors section and select Security Advisor.
  3. Find the entries about a function search path. Each entry names one function.
  4. Run the SQL query below to see all affected functions in one table.
select n.nspname as schema,
       p.proname as function_name,
       pg_get_function_identity_arguments(p.oid) as arguments,
       p.prosecdef as security_definer,
       pg_get_userbyid(p.proowner) as owner
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prokind = 'f'
  and p.proconfig is null
order by p.prosecdef desc, p.proname;

The column proconfig holds the settings that are attached to a function. If it is empty, the function has no fixed search_path. The query lists security definer functions first. Fix these functions first.

Extension functions can also appear in the list if they are in public. Do not change functions that an extension owns. If you see a name that you do not know, check if it belongs to an extension before you change it.

To see the code of one function, run this command:

select pg_get_functiondef('public.handle_new_user()'::regprocedure);

Replace the name and the arguments with your own. Read the body and list each table and function that it uses.

How do I fix the warning?

Set the search_path on each function. The linter documentation recommends an empty string: search_path = ''. With an empty path, the function finds only objects that have a schema name in the code. Postgres still searches the pg_catalog schema for built-in functions and operators.

Caution: An empty search_path can break a function that uses names without a schema. The function then fails when it runs. Test each function after the change. Do the change first in a staging project or in a transaction that you can roll back.

Follow these steps for each function:

  1. Read the function body. Use pg_get_functiondef. Find each table, view, type, and function that has no schema name.
  2. Add the schema name. Write public.profiles, not profiles. Write auth.users, not users.
  3. Set the search_path. Use alter function or create or replace function.
  4. Test the function. Run the action that calls it, such as a sign-up for a trigger function.
  5. Run the Security Advisor again. The entry for this function must disappear.

The fastest option is alter function. The Postgres documentation for ALTER FUNCTION gives the SET form. Use the full list of argument types:

alter function public.handle_new_user() set search_path = '';
alter function public.update_credits(uuid, integer) set search_path = '';

Do this only after you add the schema names in the body. To change the body, use create or replace function. The example below shows a corrected trigger function:

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  insert into public.profiles (id, email)
  values (new.id, new.email);
  return new;
end;
$$;

The old version of this function used insert into profiles. With an empty path, that name is not found. The new version uses public.profiles.

The SET clause applies when the function starts, and Postgres restores the old value when the function ends. The Postgres documentation describes this behavior. An ordinary SET command inside the function, without LOCAL, overrides the clause. Do not use it in your function bodies.

What if I do not want to write schema names?

You can set a list of trusted schemas instead of an empty string. The Postgres documentation gives this form for a security definer function: trusted schemas first, and pg_temp as the last entry.

alter function public.update_credits(uuid, integer)
  set search_path = public, pg_temp;

This form is safe if only you can create objects in public. In a Supabase project, check that the anon and authenticated roles cannot create objects in the schema. The empty string is simpler to prove safe, because no schema is searched. Use the empty string for new code.

What about functions from extensions?

Some functions come from extensions, for example pgcrypto or uuid-ossp. Supabase normally installs extensions in the extensions schema. With an empty search_path, write extensions.function_name(...) in your body. Alternatively, use a search_path that lists extensions, for example set search_path = extensions, pg_temp with a schema-qualified name for your own tables.

Does the warning matter for a function that is not security definer?

Yes, but the risk is lower. An invoker function runs with the rights of the caller. An attacker who masks a name can only make the function act with the rights that the attacker already has. The function can still return wrong results, or it can fail, when the search_path of a session differs from the one that you expect.

A fixed search_path also makes the function behave the same in every session. This helps when you debug a function that works in the SQL Editor and fails in the app. Fix these functions after the security definer functions, and use the same steps.

How do I fix many functions at once?

A project made by an AI tool can have many functions. Use a query to generate the commands, and then review them.

select format(
  'alter function %I.%I(%s) set search_path = '''';',
  n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)
)
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prokind = 'f'
  and p.proconfig is null;

Warning: Do not run all the generated commands before you check each function body. A function with names that have no schema fails after the change. A trigger function that fails can block sign-ups or inserts in your app.

Check the function bodies in this order: trigger functions on auth.users, functions that your app calls through rpc, and then the other functions. After each change, run the action that uses the function.

Who can call the function?

A fixed search_path closes one gap. It does not control who can run the function. Check the second gap now.

Postgres grants execute on new functions to PUBLIC by default. The Postgres documentation states this rule and tells you to revoke it for functions that only some users need. In Supabase, a function in an exposed schema can also be called through the Data API. The Supabase RLS documentation states that a security definer function in an exposed schema is callable over the Data API with the creator’s privileges.

Run this query to see which roles can run your functions:

select routine_name, grantee, privilege_type
from information_schema.routine_privileges
where routine_schema = 'public'
  and grantee in ('PUBLIC', 'anon', 'authenticated')
order by routine_name, grantee;

For each function that must not be public, revoke the grant:

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

A trigger function does not need execute for anon or authenticated at all. The database runs it for you. Move helper functions that your app does not call to a private schema that the Data API does not expose. Read Supabase RLS: the complete guide for the policy side of this work.

How do I check if someone used the gap?

The warning describes a possible attack. It is not a sign that an attack happened. A real attack needs a role that can create objects that the function finds. In a normal Supabase project, the API roles cannot create objects in public, but they can create temporary objects. Check these points:

  1. Check the function owner. A security definer function that is owned by postgres has the highest privileges. Note each one.
  2. Check what the function does. A function that only reads one table is a smaller risk than a function that changes roles or balances.
  3. Check who can call it. Use the grants query above.
  4. Read the API logs. Open Logs in the dashboard. Search for requests to /rest/v1/rpc/your_function. Look for callers that you do not know.
  5. Check the data that the function changes. Compare balances, roles, and flags with your own records.
  6. Check for unknown objects. List the tables and functions in public and look for names that you did not create.

If a function changes money, roles, or access, compare its results with an independent record, such as your payment provider. If the numbers differ, treat the function as abused. Read what to do about a leaked service_role key if you also see signs of key leaks.

How do I stop the warning from coming back?

Use these rules for each new function.

  1. Write set search_path = '' in every create function statement. Put the line next to language and security.
  2. Write the schema name for each table and function. Use public.table_name.
  3. Use security invoker unless you need more privilege. An invoker function runs with the rights of the caller, so RLS still applies. Use security definer only when the function must bypass RLS.
  4. Revoke execute from public in the same migration. Grant it only to the roles that need it.
  5. Keep helper functions in a private schema. Do not expose them through the Data API.
  6. Run the Security Advisor after each migration. Fix the new entries on the same day.
  7. Add the rule to your AI tool instructions. Use a sentence such as: “Every function must set search_path to an empty string and must use schema names.”
  8. Test the function after each change. Run the action that calls it, with a normal user.

The same rule covers security definer views and RLS helper functions. For the RLS case, the Supabase RLS documentation gives a pattern that sets search_path = '' in a private-schema function.

When should I get help?

Get help in these cases:

  • A function has many lines and uses many objects, and you cannot find all the names without a schema.
  • A trigger function on auth.users fails after the change, and users cannot sign up.
  • A security definer function changes money, roles, or access, and the grants allow anon.
  • The logs show calls to a function from callers that you do not know.
  • You cannot tell if a function belongs to an extension or to your own code.

After the fix, open the Security Advisor and run the checks again. Confirm that the function entries are gone and that each flow that uses a function still works.

Common questions

Is the function search_path mutable warning urgent?
It is urgent for a security definer function that is callable by anon or authenticated. For other functions it is a lower risk. Fix both kinds, and start with the security definer functions.
What value must I use for search_path?
Supabase recommends an empty string. Then write the schema name before every table and function in the body. Another safe form is a list of trusted schemas with pg_temp as the last entry.
Why does my function fail after I set search_path to an empty string?
The function body uses a name without a schema, such as a table name. Add the schema, for example public.profiles. Then run the function again.
Does the warning apply to trigger functions?
Yes. The linter reports every function in an exposed schema that has no fixed search_path. Trigger functions are included.
Can I ignore the warning if only my own code calls the function?
No. If the function is in an exposed schema, anyone can call it through the API. Set the search_path and also check who can execute the function.

Check your supabase exposure

A VibeEval scan calls your public Supabase API as an anonymous visitor and reports tables, views, and functions that return data or run without a login.

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

SCAN YOUR APP →