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:
| 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
- No Advisor, corrija primeiro as funções
SECURITY DEFINERda lista; as funções invoker correm de qualquer modo com os direitos de quem as chama. - O verificador de segurança do Supabase, gratuito, mostra as conclusões do Advisor junto das suas próprias regras, incluindo funções definer chamáveis pela API e funções definer que verificam
current_user. - Mais sobre o que as funções definer podem saltar está em NULL num IF NOT de PL/pgSQL saltou a regra em 3683 tarefas, e as políticas que chamam esses auxiliares estão em Exemplos de RLS policy no Supabase, testados pela API.