Saltar al contenido

Supabase "function search path mutable": qué significa

Supabase

Ver como MarkdownAbrir en ClaudeAbrir en ChatGPTAbrir en Perplexity

La pregunta también se copia. Si el chat se abre vacío, péguela.

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.

¿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 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.
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 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. 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:

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:

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. 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

© 2026 Denys Zaichenko. Artículo original: https://madsalt.tech/es/notes/supabase-function-search-path-mutable/