---
title: "Supabase \"function search path mutable\": o que significa"
description: "O que significa o aviso function_search_path_mutable do Supabase, quem o pode explorar, porque search_path = public não bastou no meu teste e a correção."
url: "https://madsalt.tech/pt/notes/supabase-function-search-path-mutable/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/pt/about/"
language: "pt"
published: 2026-11-01
---

# Supabase "function search path mutable": o que significa

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](https://github.com/bdougie/contributor.info/commit/3c73dda01df80faccb75278ec0f826a84e01a6d9).

## 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](https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable) 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.](https://madsalt.tech/charts/pt/supabase-function-search-path-mutable.svg)

*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](https://github.com/supabase/splinter/issues/100) 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](https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY). 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:

```sql
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:

```sql
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](https://github.com/supabase/supabase/issues/33131). 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 DEFINER` da lista; as funções invoker correm de qualquer modo com os direitos de quem as chama.
- O [verificador de segurança do Supabase](https://madsalt.tech/pt/tools/supabase-security-check/), 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](https://madsalt.tech/pt/notes/plpgsql-if-not-null-fails-open/), e as políticas que chamam esses auxiliares estão em [Exemplos de RLS policy no Supabase, testados pela API](https://madsalt.tech/pt/blog/supabase-rls-policy-examples/).
