Skip to content

Supabase "function search path mutable": what it means

Supabase

View as MarkdownOpen in ClaudeOpen in ChatGPTOpen in Perplexity

The question is copied too. If the chat opens empty, paste it.

TL;DR

The warning means a function has no fixed search_path, so every unqualified name inside it, like admins, is looked up the way the caller’s session says. In my local test a SECURITY DEFINER function is_admin() returned true for a non-admin after that user created a temporary table called admins, and pinning search_path = public did not stop it. What worked: set search_path = '' with every table written as public.admins.

Supabase’s Security Advisor lists this as lint 0011, “Function Search Path Mutable”, level WARN, with the detail “Function public.x has a role mutable search_path”. It flags every function without the setting, SECURITY INVOKER ones included, so on a project with many helpers the list gets long: one open-source project fixed 99 of them in a single migration.

What does “role mutable search_path” mean?

search_path is the list of schemas Postgres searches for a name that has no schema in front of it. A function without its own setting uses the caller’s list. Supabase’s lint page recommends pinning it to an empty string, which forces every reference inside the function to be fully qualified.

For a SECURITY DEFINER function this matters most: it runs with its owner’s rights, usually postgres, which bypasses RLS. If the caller can steer which admins it reads, the caller steers a check that runs as postgres.

Can someone exploit it in my test?

On a local Postgres 17.10 I made a table public.admins with one admin, and four versions of the same function, select exists (select 1 from admins where user_id = uid), all SECURITY DEFINER. Then I logged in as a low-privilege role that can run SQL and has the default right to create temporary tables, and created pg_temp.admins containing user A, who is not an admin:

is_admin(A) after a temp table named admins. SECURITY DEFINER function, local Postgres 17.10. No search_path: pg_temp.admins → admins → temp → true; search_path = public: pg_temp.admins → admins → temp → true; search_path = '' + public.admins: pg_temp.admins → public.admins → false.
My reproduction on a local Postgres, 24 October 2026.
Function setting Lint condition is_admin(A) after the temp table
none flagged true
set search_path = public passes true
set search_path = '', body uses public.admins passes false
set search_path = public, pg_temp passes false

The second line is the one that surprised me. A Supabase maintainer wrote in 2024 that a fixed non-empty path carries no security risk, and the lint accepts it. But Postgres searches the temporary schema first unless pg_temp is named in the path, as its docs on writing SECURITY DEFINER functions warn. So search_path = public still lets a temporary table shadow an unqualified name.

Who can actually do this on Supabase?

Not a visitor through your app. The Data API cannot create temporary tables or change the session’s search_path, so the publishable key alone does not get there. It needs a direct database connection: a second service with its own login, a BI or reporting tool, a contractor’s read-only role, a leaked connection string. On most small apps the risk is low; on any project where people or tools other than you hold database logins, fix the definer functions first.

How do you fix it?

Recreate the function with an empty path and schema names in the body:

create or replace function public.is_admin(uid uuid)
returns boolean
language sql stable security definer
set search_path = ''
as $$ select exists (select 1 from public.admins where user_id = uid) $$;

revoke execute on function public.is_admin(uuid) from public, anon;

The revoke matters as much as the path: a definer function in public is callable through the Data API, so without it any guest can ask /rpc/is_admin whether a given user is an admin. Grant it back only to the roles that need it, or keep helpers like this in a schema the API does not expose.

Do not run only alter function public.is_admin(uuid) set search_path = ''. It silences the Advisor and breaks the function: on my run the next call failed with 42P01: relation "admins" does not exist. Extension operators need their schema too, such as operator(extensions.<=>) for pgvector.

To list what the lint will flag in public, with definer functions first:

select p.proname, p.prosecdef as security_definer
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and not exists (select 1 from unnest(coalesce(p.proconfig, '{}')) c
                  where c like 'search_path=%')
order by p.prosecdef desc, p.proname;

Does the fix cost anything?

Sometimes. Any set clause stops Postgres from inlining a plain SQL function into the query that calls it, as reported on GitHub. I measured both shapes on 200,000 rows with an index on the filtered column (median of 7 runs):

SQL function, then filtered by owner No set set search_path = ''
Filter outside: visible_items() where owner = ... 0.053 ms, index used 36.7 ms, full scan
Filter inside: items_of(owner) 0.056 ms 0.082 ms

If a set-returning SQL function is queried with a filter outside it, move the filter in as a parameter before you pin the path.

Check your project

© 2026 Denys Zaichenko. Original article: https://madsalt.tech/notes/supabase-function-search-path-mutable/