Saltar para o conteúdo

Supabase "function search path mutable": o que significa

Supabase

Ver como MarkdownAbrir em ClaudeAbrir em ChatGPTAbrir em Perplexity

A pergunta também é copiada. Se o chat abrir vazio, cole-a.

Em resumo

O aviso significa que uma função não tem um search_path fixo, pelo que cada nome não qualificado lá dentro, como admins, é procurado conforme a sessão de quem a chama. No meu teste local, uma função SECURITY DEFINER is_admin() devolveu true para um utilizador sem permissões de administrador depois de esse utilizador criar uma tabela temporária chamada admins, e fixar search_path = public não o impediu. O que funcionou: set search_path = '' com cada tabela escrita como public.admins.

O Security Advisor do Supabase lista isto como o lint 0011, “Function Search Path Mutable”, nível WARN, com o detalhe “Function public.x has a role mutable search_path”. Assinala todas as funções sem a definição, incluindo as SECURITY INVOKER, por isso num projeto com muitos auxiliares a lista fica longa: um projeto de código aberto corrigiu 99 delas numa única migração.

O que significa “role mutable search_path”?

search_path é a lista de schemas em que o Postgres procura um nome que não tem schema à frente. Uma função sem definição própria usa a lista de quem a chama. A página do lint do Supabase recomenda fixá-lo numa cadeia vazia, o que obriga a qualificar por completo cada referência dentro da função.

Para uma função SECURITY DEFINER isto pesa mais: corre com os direitos do seu dono, normalmente postgres, que contorna o RLS. Se quem chama puder escolher que admins a função lê, escolhe uma verificação que corre como postgres.

Dá para explorar no meu teste?

Num Postgres 17.10 local criei uma tabela public.admins com um administrador e quatro versões da mesma função, select exists (select 1 from admins where user_id = uid), todas SECURITY DEFINER. Depois iniciei sessão com um role de poucos privilégios que pode executar SQL e tem o direito por omissão de criar tabelas temporárias, e criei pg_temp.admins com o utilizador A, que não é administrador:

is_admin(A) após uma tabela temporária admins. Função SECURITY DEFINER, Postgres 17.10 local. Sem 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.
A minha reprodução num Postgres local, 24 de outubro de 2026.
Definição da função Condição do lint is_admin(A) após a tabela temporária
nenhuma assinalada true
set search_path = public passa true
set search_path = '', o corpo usa public.admins passa false
set search_path = public, pg_temp passa false

A segunda linha foi a que me surpreendeu. Um mantenedor do Supabase escreveu em 2024 que um caminho fixo e não vazio não traz risco de segurança, e o lint aceita-o. Mas o Postgres procura primeiro no schema temporário, a não ser que pg_temp seja nomeado no caminho, como avisa a sua documentação sobre como escrever funções SECURITY DEFINER. Assim, search_path = public continua a deixar uma tabela temporária encobrir um nome não qualificado.

Quem consegue mesmo fazer isto no Supabase?

Não um visitante através da sua app. A Data API não pode criar tabelas temporárias nem alterar o search_path da sessão, por isso a chave publicável sozinha não chega lá. É preciso uma ligação direta à base de dados: um segundo serviço com o seu próprio login, uma ferramenta de BI ou de relatórios, um role só de leitura de um prestador, uma connection string que vazou. Na maioria das apps pequenas o risco é baixo; em qualquer projeto em que outras pessoas ou ferramentas além de si tenham logins na base de dados, corrija primeiro as funções definer.

Como se corrige?

Recrie a função com um caminho vazio e nomes de schema no corpo:

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;

O revoke conta tanto como o caminho: uma função definer em public pode ser chamada através da Data API, por isso, sem ele, qualquer convidado pode perguntar a /rpc/is_admin se um dado utilizador é administrador. Volte a concedê-lo apenas aos roles que precisam, ou deixe auxiliares como este num schema que a API não exponha.

Não execute apenas alter function public.is_admin(uuid) set search_path = ''. Silencia o Advisor e estraga a função: na minha execução a chamada seguinte falhou com 42P01: relation "admins" does not exist. Os operadores de extensões também precisam do seu schema, como operator(extensions.<=>) para o pgvector.

Para listar o que o lint vai assinalar em public, com as funções definer primeiro:

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;

A correção tem algum custo?

Às vezes. Qualquer cláusula set impede o Postgres de incorporar uma função SQL simples na consulta que a chama, como foi reportado no GitHub. Medi as duas formas com 200 000 linhas e um índice na coluna filtrada (mediana de 7 execuções):

Função SQL, depois filtrada por dono Sem set set search_path = ''
Filtro fora: visible_items() where owner = ... 0,053 ms, usa índice 36,7 ms, leitura completa
Filtro dentro: items_of(owner) 0,056 ms 0,082 ms

Se uma função SQL que devolve um conjunto for consultada com um filtro fora dela, passe o filtro para dentro como parâmetro antes de fixar o caminho.

Verifique o seu projeto

© 2026 Denys Zaichenko. Artigo original: https://madsalt.tech/pt/notes/supabase-function-search-path-mutable/