---
title: "Supabase Function Search Path Mutable: was die Warnung heißt"
description: "Was die Advisor-Warnung function_search_path_mutable bedeutet, wer sie ausnutzen kann, warum search_path = public im Test nicht reichte, und die Lösung."
url: "https://madsalt.tech/de/notes/supabase-function-search-path-mutable/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/de/about/"
language: "de"
published: 2026-11-01
---

# Supabase Function Search Path Mutable: was die Warnung heißt

Kurz gesagt

Die Warnung bedeutet, dass eine Funktion keinen festen `search_path` hat; jeder unqualifizierte Name darin, etwa `admins`, wird so aufgelöst, wie es die Sitzung des Aufrufers vorgibt. In meinem lokalen Test lieferte eine `SECURITY DEFINER`-Funktion `is_admin()` für einen Nicht-Admin `true`, nachdem dieser Nutzer eine temporäre Tabelle namens `admins` angelegt hatte, und `search_path = public` zu setzen verhinderte das nicht. Was funktionierte: `set search_path = ''` und jede Tabelle als `public.admins` geschrieben.

Der Security Advisor von Supabase führt das als Lint 0011, "Function Search Path Mutable", Stufe WARN, mit dem Detail "Function `public.x` has a role mutable search_path". Er meldet jede Funktion ohne die Einstellung, auch `SECURITY INVOKER`-Funktionen, sodass die Liste in einem Projekt mit vielen Helfern lang wird: Ein Open-Source-Projekt [behob 99 davon in einer einzigen Migration](https://github.com/bdougie/contributor.info/commit/3c73dda01df80faccb75278ec0f826a84e01a6d9).

## Was heißt "role mutable search_path"?

`search_path` ist die Liste der Schemas, in denen Postgres nach einem Namen ohne vorangestelltes Schema sucht. Eine Funktion ohne eigene Einstellung nutzt die Liste des Aufrufers. Die [Lint-Seite](https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable) von Supabase empfiehlt, ihn auf einen leeren String zu setzen, was jeden Verweis in der Funktion zwingt, vollständig qualifiziert zu sein.

Bei einer `SECURITY DEFINER`-Funktion ist das am wichtigsten: Sie läuft mit den Rechten ihres Eigentümers, meist `postgres`, und der umgeht RLS. Wenn der Aufrufer steuern kann, welches `admins` sie liest, steuert er eine Prüfung, die als `postgres` läuft.

## Lässt sich das in meinem Test ausnutzen?

Auf einem lokalen Postgres 17.10 legte ich eine Tabelle `public.admins` mit einem Admin an und vier Versionen derselben Funktion, `select exists (select 1 from admins where user_id = uid)`, alle `SECURITY DEFINER`. Dann meldete ich mich als Rolle mit niedrigen Rechten an, die SQL ausführen darf und standardmäßig temporäre Tabellen anlegen kann, und legte `pg_temp.admins` mit Nutzer A an, der kein Admin ist:

![is_admin(A) nach Temp-Tabelle admins. SECURITY DEFINER-Funktion, lokales Postgres 17.10. Kein 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/de/supabase-function-search-path-mutable.svg)

*Meine Nachstellung auf einem lokalen Postgres, 24. Oktober 2026.*

| Einstellung der Funktion                           | Lint-Bedingung | `is_admin(A)` nach der temporären Tabelle |
| -------------------------------------------------- | -------------- | ----------------------------------------- |
| keine                                              | gemeldet       | `true`                                    |
| `set search_path = public`                         | bestanden      | `true`                                    |
| `set search_path = ''`, Body nutzt `public.admins` | bestanden      | `false`                                   |
| `set search_path = public, pg_temp`                | bestanden      | `false`                                   |

Die zweite Zeile überraschte mich. Ein Supabase-Maintainer [schrieb 2024](https://github.com/supabase/splinter/issues/100), ein fester nicht leerer Pfad berge kein Sicherheitsrisiko, und der Lint akzeptiert ihn. Aber Postgres durchsucht das temporäre Schema zuerst, sofern `pg_temp` nicht im Pfad genannt ist, wie es die [Dokumentation zum Schreiben von SECURITY-DEFINER-Funktionen](https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY) warnt. Mit `search_path = public` kann also weiterhin eine temporäre Tabelle einen unqualifizierten Namen überdecken.

## Wer kann das bei Supabase tatsächlich tun?

Kein Besucher über Ihre App. Die Data API kann weder temporäre Tabellen anlegen noch den `search_path` der Sitzung ändern, der publishable key allein führt also nicht dorthin. Nötig ist eine direkte Datenbankverbindung: ein zweiter Dienst mit eigenem Login, ein BI- oder Reporting-Tool, die schreibgeschützte Rolle eines Auftragnehmers, ein geleakter Connection-String. Bei den meisten kleinen Apps ist das Risiko gering; in jedem Projekt, in dem andere Personen oder Tools als Sie Datenbank-Logins haben, beheben Sie zuerst die Definer-Funktionen.

## Wie beheben Sie das?

Legen Sie die Funktion mit leerem Pfad und Schemanamen im Body neu an:

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

Das `revoke` ist so wichtig wie der Pfad: Eine Definer-Funktion in `public` ist über die Data API aufrufbar; ohne es kann jeder Gast bei `/rpc/is_admin` fragen, ob ein bestimmter Nutzer Admin ist. Vergeben Sie das Recht nur an die Rollen zurück, die es brauchen, oder legen Sie solche Helfer in ein Schema, das die API nicht exponiert.

Führen Sie nicht nur `alter function public.is_admin(uuid) set search_path = ''` aus. Das bringt den Advisor zum Schweigen und zerstört die Funktion: Bei meinem Lauf scheiterte der nächste Aufruf mit `42P01: relation "admins" does not exist`. Auch Operatoren aus Erweiterungen brauchen ihr Schema, etwa `operator(extensions.<=>)` für pgvector.

So listen Sie auf, was der Lint in `public` meldet, Definer-Funktionen zuerst:

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

## Kostet die Lösung etwas?

Manchmal. Jede `set`-Klausel hindert Postgres daran, eine einfache SQL-Funktion in die aufrufende Abfrage einzubetten, wie [auf GitHub berichtet](https://github.com/supabase/supabase/issues/33131). Ich habe beide Formen auf 200.000 Zeilen mit einem Index auf der gefilterten Spalte gemessen (Median aus 7 Läufen):

| SQL-Funktion, danach nach Besitzer gefiltert          | Ohne `set`              | `set search_path = ''` |
| ----------------------------------------------------- | ----------------------- | ---------------------- |
| Filter außerhalb: `visible_items() where owner = ...` | 0,053 ms, Index genutzt | 36,7 ms, Full Scan     |
| Filter innerhalb: `items_of(owner)`                   | 0,056 ms                | 0,082 ms               |

Wird eine mengenliefernde SQL-Funktion mit einem Filter außerhalb abgefragt, verschieben Sie den Filter als Parameter hinein, bevor Sie den Pfad festlegen.

## Prüfen Sie Ihr Projekt

- Beheben Sie im Advisor zuerst die `SECURITY DEFINER`-Funktionen auf der Liste; Invoker-Funktionen laufen ohnehin mit den Rechten des Aufrufers.
- Der kostenlose [Supabase-Sicherheitscheck](https://madsalt.tech/de/tools/supabase-security-check/) zeigt Advisor-Funde neben seinen eigenen Regeln, darunter über die API aufrufbare Definer-Funktionen und Definer-Funktionen, die `current_user` prüfen.
- Mehr dazu, was Definer-Funktionen überspringen können, steht in [NULL im PL/pgSQL-IF NOT: Prüfung bei 3683 Tasks übersprungen](https://madsalt.tech/de/notes/plpgsql-if-not-null-fails-open/), und Policies, die solche Helfer aufrufen, in [Supabase RLS Policy Beispiele, je per API getestet](https://madsalt.tech/de/blog/supabase-rls-policy-examples/).
