---
title: "Supabase Function Search Path Mutable: hvad det betyder"
description: "Hvad Advisor-advarslen function_search_path_mutable betyder, hvem der kan udnytte den, hvorfor search_path = public ikke var nok i min test, og rettelsen."
url: "https://madsalt.tech/da/notes/supabase-function-search-path-mutable/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/da/about/"
language: "da"
published: 2026-11-01
---

# Supabase Function Search Path Mutable: hvad det betyder

TL;DR

Advarslen betyder, at en funktion ikke har en fast `search_path`, så hvert ukvalificeret navn i den, som `admins`, slås op, som den kaldendes session angiver. I min lokale test returnerede en `SECURITY DEFINER`-funktion `is_admin()` `true` for en ikke-admin, efter at brugeren havde oprettet en midlertidig tabel kaldet `admins`, og at låse `search_path = public` stoppede det ikke. Det, der virkede: `set search_path = ''` med hver tabel skrevet som `public.admins`.

Supabases Security Advisor angiver dette som lint 0011, "Function Search Path Mutable", niveau WARN, med detaljen "Function `public.x` has a role mutable search_path". Den flager hver funktion uden indstillingen, også `SECURITY INVOKER`-funktioner, så på et projekt med mange hjælpere bliver listen lang: et open source-projekt [rettede 99 af dem i én enkelt migration](https://github.com/bdougie/contributor.info/commit/3c73dda01df80faccb75278ec0f826a84e01a6d9).

## Hvad betyder "role mutable search_path"?

`search_path` er listen over skemaer, Postgres leder i efter et navn uden skema foran. En funktion uden egen indstilling bruger den kaldendes liste. Supabases [lint-side](https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable) anbefaler at sætte den til en tom streng, hvilket tvinger hver reference i funktionen til at være fuldt kvalificeret.

For en `SECURITY DEFINER`-funktion betyder det mest: den kører med sin ejers rettigheder, normalt `postgres`, som omgår RLS. Hvis den kaldende kan styre, hvilken `admins` den læser, styrer vedkommende et tjek, der kører som `postgres`.

## Kan nogen udnytte det i min test?

På en lokal Postgres 17.10 lavede jeg en tabel `public.admins` med én admin og fire versioner af den samme funktion, `select exists (select 1 from admins where user_id = uid)`, alle `SECURITY DEFINER`. Derefter loggede jeg ind som en rolle med lave rettigheder, der kan køre SQL og har standardretten til at oprette midlertidige tabeller, og oprettede `pg_temp.admins` med bruger A, som ikke er admin:

![is_admin(A) efter en temp-tabel admins. SECURITY DEFINER-funktion, lokal Postgres 17.10. Ingen 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/da/supabase-function-search-path-mutable.svg)

*Min gengivelse på en lokal Postgres, 24. oktober 2026.*

| Funktionens indstilling                                | Lint-betingelse | `is_admin(A)` efter den midlertidige tabel |
| ------------------------------------------------------ | --------------- | ------------------------------------------ |
| ingen                                                  | flagget         | `true`                                     |
| `set search_path = public`                             | består          | `true`                                     |
| `set search_path = ''`, kroppen bruger `public.admins` | består          | `false`                                    |
| `set search_path = public, pg_temp`                    | består          | `false`                                    |

Den anden linje er den, der overraskede mig. En Supabase-maintainer [skrev i 2024](https://github.com/supabase/splinter/issues/100), at en fast ikke-tom sti ikke indebærer nogen sikkerhedsrisiko, og linten accepterer den. Men Postgres leder i det midlertidige skema først, medmindre `pg_temp` er nævnt i stien, som dens [dokumentation om at skrive SECURITY DEFINER-funktioner](https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY) advarer om. Så med `search_path = public` kan en midlertidig tabel stadig skygge for et ukvalificeret navn.

## Hvem kan faktisk gøre det på Supabase?

Ikke en besøgende gennem din app. Data API'et kan hverken oprette midlertidige tabeller eller ændre sessionens `search_path`, så publishable key alene når ikke dertil. Det kræver en direkte databaseforbindelse: en anden tjeneste med eget login, et BI- eller rapporteringsværktøj, en konsulents skrivebeskyttede rolle, en lækket forbindelsesstreng. På de fleste små apps er risikoen lav; på ethvert projekt, hvor andre personer eller værktøjer end dig har databaselogins, så ret definer-funktionerne først.

## Hvordan retter du det?

Opret funktionen igen med en tom sti og skemanavne i kroppen:

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

`revoke` betyder lige så meget som stien: en definer-funktion i `public` kan kaldes gennem Data API'et, så uden den kan enhver gæst spørge `/rpc/is_admin`, om en given bruger er admin. Giv den kun tilbage til de roller, der har brug for den, eller hold hjælpere som denne i et skema, API'et ikke eksponerer.

Kør ikke kun `alter function public.is_admin(uuid) set search_path = ''`. Det får Advisor til at tie og ødelægger funktionen: i min kørsel fejlede næste kald med `42P01: relation "admins" does not exist`. Operatorer fra udvidelser har også brug for deres skema, som `operator(extensions.<=>)` til pgvector.

Sådan lister du det, linten vil flage i `public`, med definer-funktioner først:

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

## Koster rettelsen noget?

Nogle gange. Enhver `set`-klausul forhindrer Postgres i at indlejre en simpel SQL-funktion i den forespørgsel, der kalder den, [som rapporteret på GitHub](https://github.com/supabase/supabase/issues/33131). Jeg målte begge former på 200.000 rækker med et indeks på den filtrerede kolonne (median af 7 kørsler):

| SQL-funktion, derefter filtreret på ejer            | Uden `set`             | `set search_path = ''` |
| --------------------------------------------------- | ---------------------- | ---------------------- |
| Filter udenfor: `visible_items() where owner = ...` | 0,053 ms, indeks brugt | 36,7 ms, fuld scanning |
| Filter indeni: `items_of(owner)`                    | 0,056 ms               | 0,082 ms               |

Hvis en funktion, der returnerer et sæt, forespørges med et filter udenfor, så flyt filteret ind som parameter, før du låser stien.

## Tjek dit projekt

- Ret i Advisor først `SECURITY DEFINER`-funktionerne på listen; invoker-funktioner kører alligevel med den kaldendes rettigheder.
- Det gratis [Supabase-sikkerhedstjek](https://madsalt.tech/da/tools/supabase-security-check/) viser Advisor-fund ved siden af sine egne regler, herunder definer-funktioner, der kan kaldes gennem API'et, og definer-funktioner, der tjekker `current_user`.
- Mere om, hvad definer-funktioner kan springe over, står i [En NULL i PL/pgSQL IF NOT sprang tjek over for 3683 opgaver](https://madsalt.tech/da/notes/plpgsql-if-not-null-fails-open/), og policies, der kalder sådanne hjælpere, i [Supabase RLS policy-eksempler, hver testet via API](https://madsalt.tech/da/blog/supabase-rls-policy-examples/).
