---
title: "Supabase security definer view: hvorfor det omgår RLS"
description: "Hvorfor et Supabase-view returnerer rækker, som RLS skjuler på tabellen, hvad Advisor-advarslen security_definer_view betyder, og indstillingen der løser det."
url: "https://madsalt.tech/da/notes/supabase-security-definer-view/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/da/about/"
language: "da"
published: 2026-11-02
---

# Supabase security definer view: hvorfor det omgår RLS

TL;DR

Et view i Postgres kører med sin ejers rettigheder, medmindre det er oprettet med `security_invoker = true`. I Supabase er ejeren som regel `postgres`, som omgår RLS, så et view over en beskyttet tabel returnerer rækker, som tabellen selv skjuler. På min lokale stack fik den offentlige nøgle 0 rækker fra tabellen og alle 3 af 3 fra et almindeligt view over den; med `security_invoker = true` returnerede viewet også 0. På Postgres 15 og nyere bør du sætte den på hvert view i et eksponeret schema.

Mit gratis [Supabase-sikkerhedstjek](https://madsalt.tech/da/tools/supabase-security-check/) har en regel, der hedder "Views, der omgår RLS", og Supabases Security Advisor melder det samme som `security_definer_view`. Det er let at overse, fordi den tabel, der lækker, ser beskyttet ud.

## Hvorfor omgår et Supabase-view RLS?

Et view er en gemt forespørgsel. Som standard tjekker Postgres rettigheder og RLS på tabellerne i det som viewets ejer, ikke som den, der kalder det. Supabases [RLS-guide](https://supabase.com/docs/guides/database/postgres/row-level-security) siger det direkte: views omgår RLS som standard, fordi de som regel oprettes med brugeren `postgres`, og et view over en beskyttet tabel udleverer hver række, som tabellens policies skulle have holdt tilbage.

Data API'et leverer views som tabeller. Hvis viewet ligger i et eksponeret schema, og `anon` eller `authenticated` kan vælge fra det, læser den offentlige nøgle det.

## Hvad returnerede viewet i min test?

På en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) oprettede jeg en tabel `orders` med tre falske kunder, aktiverede RLS uden policy og oprettede et view `order_summary` over den med en `select`-rettighed til `anon`. Derefter kaldte jeg begge med publishable key:

![Samme tabel, tre måder at læse den på. Lokal Supabase, RLS slået til uden policy, publishable key. Selve tabellen: Publishable key → orders, RLS aktiv → 0 rækker; Et almindeligt view over den: Publishable key → view order_summary → 3 af 3 rækker; Viewet med security_invoker: Publishable key → view security_invoker → 0 rækker.](https://madsalt.tech/charts/da/supabase-security-definer-view.svg)

*Min genskabelse på en lokal Supabase, 23. oktober 2026.*

| Anmodning med publishable key                                | Resultat                            |
| ------------------------------------------------------------ | ----------------------------------- |
| `GET /rest/v1/orders` (RLS slået til, ingen policy)          | `[]`                                |
| `GET /rest/v1/order_summary` (almindeligt view)              | alle 3 rækker, med navne og e-mails |
| `GET /rest/v1/order_summary` efter `security_invoker = true` | `[]`                                |

Samme tabel, samme nøgle. Kun viewets indstilling ændrede, hvad der kom tilbage.

## Hvordan retter du det?

På Postgres 15 og nyere, som alle nuværende Supabase-projekter kører, kan du få viewet til at tjekke RLS som den, der kalder:

```sql
alter view public.order_summary set (security_invoker = true);
```

Til et nyt view skriver du det i definitionen:

```sql
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
```

Den, der kalder, skal så også have `select` på den underliggende tabel, og tabellens policies afgør rækkerne. Hvis et view skal aggregere data, som den, der kalder, ikke må se række for række, for eksempel et offentligt antal, så hold det uden for det eksponerede schema og lever det gennem en funktion med et snævert resultat.

Sådan lister du de views i `public`, der stadig kører som deres ejer:

```sql
select c.relname as view
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
  and not coalesce(
    (select bool_or(option = 'security_invoker=true' or option = 'security_invoker=on')
     from unnest(c.reloptions) as option), false);
```

På min stack, med ét almindeligt view og to med `security_invoker = true`, returnerede den kun det almindelige.

Materialized views understøtter slet ikke RLS, så tjekket flagger dem i API'et for sig, under "Materialized views eller foreign tables i API’et". Hvad der ellers går uden om policies, fra secret key til `SECURITY DEFINER`-funktioner, står i min [sikkerhedstjekliste til Lovable-apps](https://madsalt.tech/da/blog/lovable-app-security-checklist/), og hvordan du beviser selve policies, står i [sådan tester jeg Supabase RLS](https://madsalt.tech/da/blog/how-to-test-supabase-rls/). En policy, der slår teams op gennem en hjælpefunktion uden rekursion, står i [Supabase RLS policy-eksempler, hver testet via API](https://madsalt.tech/da/blog/supabase-rls-policy-examples/). En security definer-funktion har brug for en indstilling mere, en fast `search_path`; hvorfor `search_path = public` ikke var nok i min test, står i [Supabase Function Search Path Mutable: hvad det betyder](https://madsalt.tech/da/notes/supabase-function-search-path-mutable/).
