---
title: "Supabase \"function search path mutable\": qué significa"
description: "Qué significa el aviso function_search_path_mutable de Supabase, quién puede explotarlo, por qué search_path = public no bastó en mi prueba y la solución."
url: "https://madsalt.tech/es/notes/supabase-function-search-path-mutable/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/es/about/"
language: "es"
published: 2026-11-01
---

# Supabase "function search path mutable": qué significa

En resumen

El aviso significa que una función no tiene un `search_path` fijo, así que cada nombre sin calificar dentro de ella, como `admins`, se busca como lo indique la sesión de quien la llama. En mi prueba local, una función `SECURITY DEFINER` `is_admin()` devolvió `true` para un usuario sin permisos de administrador después de que ese usuario creara una tabla temporal llamada `admins`, y fijar `search_path = public` no lo evitó. Lo que funcionó: `set search_path = ''` con cada tabla escrita como `public.admins`.

El Security Advisor de Supabase lo lista como el lint 0011, "Function Search Path Mutable", nivel WARN, con el detalle "Function `public.x` has a role mutable search_path". Marca todas las funciones sin el ajuste, incluidas las `SECURITY INVOKER`, así que en un proyecto con muchos ayudantes la lista se alarga: un proyecto de código abierto [corrigió 99 de ellas en una sola migración](https://github.com/bdougie/contributor.info/commit/3c73dda01df80faccb75278ec0f826a84e01a6d9).

## ¿Qué significa "role mutable search_path"?

`search_path` es la lista de esquemas en la que Postgres busca un nombre que no lleva esquema delante. Una función sin ajuste propio usa la lista de quien la llama. La [página del lint](https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable) de Supabase recomienda fijarlo en una cadena vacía, lo que obliga a calificar por completo cada referencia dentro de la función.

Para una función `SECURITY DEFINER` esto importa más: se ejecuta con los permisos de su propietario, normalmente `postgres`, que se salta RLS. Si quien llama puede dirigir qué `admins` lee la función, dirige una comprobación que se ejecuta como `postgres`.

## ¿Se puede explotar en mi prueba?

En un Postgres 17.10 local creé una tabla `public.admins` con un administrador y cuatro versiones de la misma función, `select exists (select 1 from admins where user_id = uid)`, todas `SECURITY DEFINER`. Después inicié sesión con un rol de pocos privilegios que puede ejecutar SQL y tiene el derecho por defecto a crear tablas temporales, y creé `pg_temp.admins` con el usuario A, que no es administrador:

![is_admin(A) tras una tabla temporal admins. Función SECURITY DEFINER, Postgres 17.10 local. Sin 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/es/supabase-function-search-path-mutable.svg)

*Mi reproducción en un Postgres local, 24 de octubre de 2026.*

| Ajuste de la función                                  | Condición del lint | `is_admin(A)` tras la tabla temporal |
| ----------------------------------------------------- | ------------------ | ------------------------------------ |
| ninguno                                               | marcada            | `true`                               |
| `set search_path = public`                            | pasa               | `true`                               |
| `set search_path = ''`, el cuerpo usa `public.admins` | pasa               | `false`                              |
| `set search_path = public, pg_temp`                   | pasa               | `false`                              |

La segunda línea es la que me sorprendió. Un mantenedor de Supabase [escribió en 2024](https://github.com/supabase/splinter/issues/100) que una ruta fija no vacía no conlleva riesgo de seguridad, y el lint la acepta. Pero Postgres busca primero en el esquema temporal salvo que `pg_temp` se nombre en la ruta, como advierte su [documentación sobre cómo escribir funciones SECURITY DEFINER](https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY). Así que `search_path = public` sigue dejando que una tabla temporal tape un nombre sin calificar.

## ¿Quién puede hacerlo realmente en Supabase?

No un visitante a través de su app. La Data API no puede crear tablas temporales ni cambiar el `search_path` de la sesión, así que la clave publicable por sí sola no llega hasta ahí. Hace falta una conexión directa a la base de datos: un segundo servicio con su propio login, una herramienta de BI o de informes, un rol de solo lectura de un contratista, una cadena de conexión filtrada. En la mayoría de las apps pequeñas el riesgo es bajo; en cualquier proyecto donde otras personas o herramientas además de usted tengan logins de base de datos, corrija primero las funciones definer.

## ¿Cómo se corrige?

Recree la función con una ruta vacía y nombres de esquema en el cuerpo:

```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;
```

El `revoke` importa tanto como la ruta: una función definer en `public` se puede llamar a través de la Data API, así que sin él cualquier invitado puede preguntar a `/rpc/is_admin` si un usuario dado es administrador. Vuelva a concederlo solo a los roles que lo necesiten, o deje ayudantes como este en un esquema que la API no exponga.

No ejecute solo `alter function public.is_admin(uuid) set search_path = ''`. Silencia el Advisor y rompe la función: en mi ejecución la siguiente llamada falló con `42P01: relation "admins" does not exist`. Los operadores de extensiones también necesitan su esquema, como `operator(extensions.<=>)` para pgvector.

Para listar lo que el lint marcará en `public`, con las funciones definer primero:

```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;
```

## ¿Tiene algún coste la corrección?

A veces. Cualquier cláusula `set` impide que Postgres incorpore una función SQL simple a la consulta que la llama, como se [informó en GitHub](https://github.com/supabase/supabase/issues/33131). Medí ambas formas con 200.000 filas y un índice en la columna filtrada (mediana de 7 ejecuciones):

| Función SQL, luego filtrada por propietario       | Sin `set`            | `set search_path = ''`    |
| ------------------------------------------------- | -------------------- | ------------------------- |
| Filtro fuera: `visible_items() where owner = ...` | 0,053 ms, usa índice | 36,7 ms, lectura completa |
| Filtro dentro: `items_of(owner)`                  | 0,056 ms             | 0,082 ms                  |

Si una función SQL que devuelve un conjunto se consulta con un filtro fuera de ella, mueva el filtro dentro como parámetro antes de fijar la ruta.

## Compruebe su proyecto

- En el Advisor, corrija primero las funciones `SECURITY DEFINER` de la lista; las funciones invoker se ejecutan con los permisos de quien llama de todos modos.
- El [verificador de seguridad de Supabase](https://madsalt.tech/es/tools/supabase-security-check/), gratuito, muestra los hallazgos del Advisor junto a sus propias reglas, incluidas las funciones definer invocables por la API y las funciones definer que comprueban `current_user`.
- Más sobre lo que pueden saltarse las funciones definer está en [NULL en IF NOT de PL/pgSQL: saltó el control en 3683 tareas](https://madsalt.tech/es/notes/plpgsql-if-not-null-fails-open/), y las políticas que llaman a esos ayudantes están en [Políticas RLS de Supabase: ejemplos probados con la API](https://madsalt.tech/es/blog/supabase-rls-policy-examples/).
