Aller au contenu

Supabase Function Search Path Mutable : ce que ça signifie

Supabase

Voir en MarkdownOuvrir dans ClaudeOuvrir dans ChatGPTOuvrir dans Perplexity

La question est aussi copiée. Si le chat reste vide, collez-la.

En bref

L’alerte signifie qu’une fonction n’a pas de search_path fixe ; chaque nom non qualifié qu’elle contient, comme admins, est résolu comme le dit la session de l’appelant. Dans mon test local, une fonction SECURITY DEFINER is_admin() a renvoyé true pour un non-admin après que cet utilisateur a créé une table temporaire nommée admins, et fixer search_path = public ne l’a pas empêché. Ce qui a marché : set search_path = '' avec chaque table écrite public.admins.

Le Security Advisor de Supabase liste cela comme le lint 0011, « Function Search Path Mutable », niveau WARN, avec le détail « Function public.x has a role mutable search_path ». Il signale chaque fonction sans ce réglage, y compris les fonctions SECURITY INVOKER ; sur un projet qui a beaucoup d’aides, la liste devient longue : un projet open source en a corrigé 99 dans une seule migration.

Que signifie « role mutable search_path » ?

search_path est la liste des schémas dans lesquels Postgres cherche un nom sans schéma devant lui. Une fonction sans réglage propre utilise la liste de l’appelant. La page du lint de Supabase recommande de le fixer à une chaîne vide, ce qui oblige chaque référence dans la fonction à être entièrement qualifiée.

Pour une fonction SECURITY DEFINER, c’est le plus important : elle s’exécute avec les droits de son propriétaire, généralement postgres, qui contourne la RLS. Si l’appelant peut orienter quel admins elle lit, il oriente un contrôle exécuté en tant que postgres.

Peut-on l’exploiter dans mon test ?

Sur un Postgres 17.10 local, j’ai créé une table public.admins avec un admin et quatre versions de la même fonction, select exists (select 1 from admins where user_id = uid), toutes SECURITY DEFINER. Puis je me suis connecté avec un rôle à faibles privilèges qui peut exécuter du SQL et a le droit par défaut de créer des tables temporaires, et j’ai créé pg_temp.admins contenant l’utilisateur A, qui n’est pas admin :

is_admin(A) après une table temp admins. Fonction SECURITY DEFINER, Postgres 17.10 local. Aucun 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.
Ma reproduction sur un Postgres local, 24 octobre 2026.
Réglage de la fonction Condition du lint is_admin(A) après la table temporaire
aucun signalée true
set search_path = public passe true
set search_path = '', le corps utilise public.admins passe false
set search_path = public, pg_temp passe false

La deuxième ligne est celle qui m’a surpris. Un mainteneur de Supabase a écrit en 2024 qu’un chemin fixe non vide ne comporte aucun risque de sécurité, et le lint l’accepte. Mais Postgres cherche d’abord dans le schéma temporaire, sauf si pg_temp est nommé dans le chemin, comme l’avertit sa documentation sur l’écriture de fonctions SECURITY DEFINER. Avec search_path = public, une table temporaire peut donc encore masquer un nom non qualifié.

Qui peut vraiment le faire sur Supabase ?

Pas un visiteur via votre application. La Data API ne peut ni créer de tables temporaires ni changer le search_path de la session ; la clé publishable seule n’y mène donc pas. Il faut une connexion directe à la base : un second service avec son propre identifiant, un outil de BI ou de reporting, le rôle en lecture seule d’un prestataire, une chaîne de connexion divulguée. Sur la plupart des petites applications, le risque est faible ; sur tout projet où d’autres personnes ou outils que vous détiennent des identifiants de base, corrigez d’abord les fonctions definer.

Comment le corriger ?

Recréez la fonction avec un chemin vide et des noms de schéma dans le corps :

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;

Le revoke compte autant que le chemin : une fonction definer dans public est appelable via la Data API ; sans lui, n’importe quel invité peut demander à /rpc/is_admin si un utilisateur donné est admin. Ne le rendez qu’aux rôles qui en ont besoin, ou gardez ces aides dans un schéma que l’API n’expose pas.

N’exécutez pas seulement alter function public.is_admin(uuid) set search_path = ''. Cela fait taire l’Advisor et casse la fonction : lors de mon essai, l’appel suivant a échoué avec 42P01: relation "admins" does not exist. Les opérateurs d’extension ont aussi besoin de leur schéma, comme operator(extensions.<=>) pour pgvector.

Pour lister ce que le lint signalera dans public, fonctions definer en premier :

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;

La correction a-t-elle un coût ?

Parfois. Toute clause set empêche Postgres d’intégrer une fonction SQL simple dans la requête qui l’appelle, comme signalé sur GitHub. J’ai mesuré les deux formes sur 200 000 lignes avec un index sur la colonne filtrée (médiane de 7 exécutions) :

Fonction SQL, puis filtrée par propriétaire Sans set set search_path = ''
Filtre à l’extérieur : visible_items() where owner = ... 0,053 ms, index utilisé 36,7 ms, scan complet
Filtre à l’intérieur : items_of(owner) 0,056 ms 0,082 ms

Si une fonction SQL qui renvoie un ensemble est interrogée avec un filtre à l’extérieur, déplacez le filtre à l’intérieur comme paramètre avant de fixer le chemin.

Vérifiez votre projet

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