Záchrana · 9 min čítania
Checklist Supabase Row Level Security pre aplikácie postavené s AI (s SQL)
Ak vaša aplikácia komunikuje so Supabase priamo z prehliadača, Row Level Security je vaša kontrola prístupu. Zapnite RLS na každej vystavenej tabuľke, napíšte jednu politiku na operáciu s auth.uid(), kľúč service_role držte na serveri, zabezpečte storage buckety a testujte ako anonymný používateľ aj ako druhý účet. SQL na skopírovanie nájdete nižšie.
Autor BacklooPublikované
Hlavné body
- Anon (publishable) kľúč je verejný už zo svojej podstaty. Medzi ním a vašimi dátami stojí iba RLS.
- Kľúč `service_role` (secret) úplne obchádza RLS a nikdy sa nesmie dostať do prehliadača ani do mobilnej aplikácie.
- Zapnite RLS na každej tabuľke vo vystavenej schéme a potom napíšte explicitné politiky pre select, insert, update a delete.
- Roly patria do `app_metadata`, nie do `user_metadata`; views, funkcie so `security definer` a storage potrebujú vlastné kontroly.
- Politiky testujte tak, že sa vydávate za `anon` a za druhého používateľa — nie preklikávaním aplikácie za seba.
Prečo je Row Level Security v aplikáciách postavených s AI taká dôležitá?
Pretože nástroje ako Lovable a Bolt generujú aplikácie, v ktorých sa prehliadač pýta Supabase priamo. Medzi nimi nie je žiadny backend, ktorý by rozhodoval, kto čo smie vidieť. Požiadavka nesie verejný kľúč a, ak je používateľ prihlásený, aj jeho JWT. Postgres potom uplatní politiky Row Level Security. Ak je RLS vypnuté alebo politika hovorí true, ktokoľvek s verejným kľúčom — ktorý je vo vašom JavaScripte — môže tabuľku čítať alebo meniť.
Vo veľkom sa to už stalo. Chýbajúce RLS odhalilo dáta vo viac ako 170 aplikáciách postavených v Lovable, evidované ako CVE-2025-48757.
170+
Dobrá správa: RLS je obyčajné SQL, ľahko sa audituje a jeho oprava si len zriedka vyžaduje zásah do frontendu. Prejdite si sekcie nižšie v poradí.
Anon kľúč vs. kľúč service_role: ktorý patrí kam?
Anon kľúč (v novších projektoch nazývaný publishable) patrí do klienta; kľúč service_role (secret) zostáva na serveri. Anon kľúč zodpovedá postgresovej role anon, po prihlásení používateľa role authenticated, a na obe sa vzťahuje RLS. Kľúč service_role zodpovedá role, ktorá RLS úplne obchádza.
| Kľúč | Rola v Postgrese | Platí RLS? | Kde môže byť |
|---|---|---|---|
| anon / publishable | anon, potom authenticated | Áno | Prehliadač, mobilná aplikácia, verejný repozitár |
| service_role / secret | service_role | Nie — obchádza RLS | Iba server, edge funkcie, tajné premenné v CI |
Častá skratka v kóde od AI: dotaz zlyhá kvôli RLS a „oprava“ v klientovi vymení kľúč za service role. Chyba zmizne — a s ňou celá vaša kontrola prístupu. Prehľadajte repozitár aj produkčný bundle, či neobsahujú kľúč service role alebo akúkoľvek premennú prostredia s názvom ako SERVICE_ROLE. Ak sa niekedy dostal do prehliadača, vymeňte ho.
Serverový kód, ktorý koná v mene používateľa
Edge Functions a API routes nepotrebujú tajný kľúč automaticky. Keď funkcia koná v mene prihláseného používateľa, vytvorte klienta s verejným kľúčom a prepošlite hlavičku Authorization od používateľa. Postgres potom vidí JWT používateľa a uplatní RLS presne tak ako pri požiadavke z prehliadača. Tajný kľúč si nechajte na prácu, pri ktorej naozaj nie je žiadny používateľ, napríklad webhooky a plánované úlohy.
// Supabase Edge Function: dotaz v mene volajúceho, takže RLS stále platí
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);
const { data, error } = await supabase.from("projects").select("id, name");
if (error) return new Response(error.message, { status: 400 });
return Response.json(data);
});Krok 1: zapnite RLS na každej vystavenej tabuľke
Každá tabuľka v schéme vystavenej cez Data API (štandardne public) potrebuje zapnuté RLS. Tabuľky vytvorené v editore tabuliek v administrácii ho majú zapnuté automaticky; tabuľky vytvorené čistým SQL alebo migráciami — čo je spôsob, akým ich vytvára väčšina AI nástrojov — ho nemajú, pokiaľ to SQL výslovne neuvádza. Začnite výpisom medzier:
-- Tabuľky v schéme public s vypnutým RLS
select schemaname, tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;
-- Tabuľky so zapnutým RLS, ale bez akýchkoľvek politík (všetko je zakázané — často znak,
-- že to niekto neskôr „opravil“ kľúčom service_role)
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'r'
and c.relrowsecurity
and not exists (select 1 from pg_policies p
where p.schemaname = 'public' and p.tablename = c.relname);alter table public.projects enable row level security;So zapnutým RLS a bez politík tabuľka rolám anon a authenticated všetko zakazuje. To je bezpečné východisko; teraz ju zámerne otvárate. Tabuľky bez RLS označí aj Security Advisor v administrácii Supabase — spustite ho, ale čistý report neberte ako dôkaz, že vaše politiky sú správne.
Krok 2: napíšte jednu politiku na každú operáciu
Namiesto jednej politiky for all napíšte samostatné politiky pre select, insert, update a delete, každú obmedzenú na konkrétnu rolu. Samostatné politiky sa ľahšie kontrolujú a nútia vás rozhodnúť, či používateľ smie riadky vôbec mazať. using filtruje, ktoré existujúce riadky môže používateľ vidieť alebo meniť; with check validuje nový zapisovaný riadok.
-- Predpokladá stĺpec vlastníka, ktorý má predvolene hodnotu prihláseného používateľa:
-- owner_id uuid not null default auth.uid() references auth.users (id) on delete cascade
create index if not exists projects_owner_id_idx on public.projects (owner_id);
create policy "projects: owners can read"
on public.projects for select
to authenticated
using ( (select auth.uid()) = owner_id );
create policy "projects: owners can create"
on public.projects for insert
to authenticated
with check ( (select auth.uid()) = owner_id );
create policy "projects: owners can update"
on public.projects for update
to authenticated
using ( (select auth.uid()) = owner_id )
with check ( (select auth.uid()) = owner_id );
create policy "projects: owners can delete"
on public.projects for delete
to authenticated
using ( (select auth.uid()) = owner_id );Dva detaily sa oplatí prevziať. to authenticated znamená, že pri anonymných požiadavkách sa politika ani nevyhodnocuje. A obalenie auth.uid() do (select ...) umožní Postgresu vyhodnotiť ho raz na dotaz namiesto raz na riadok, čo je pri veľkých tabuľkách podstatné. Indexujte každý stĺpec, podľa ktorého vaše politiky filtrujú.
-- Povoľte používateľom meniť na profile iba konkrétne stĺpce
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;Vzory s auth.uid(): vlastníctvo, tímy a roly
Takmer každá politika má jeden z troch tvarov: riadok patrí používateľovi, riadok patrí tímu, v ktorom je používateľ, alebo používateľ má určitú rolu. Vlastníctvo je príklad s owner_id vyššie. Tímy a roly si vyžadujú trochu viac pozornosti.
Členstvo v tíme
Kontrolu členstva dajte do funkcie so security definer v schéme, ktorá nie je vystavená cez API. Vyhnete sa tak rekurzívnym politikám na tabuľke členstiev a vyhľadávanie zostane rýchle.
create schema if not exists private;
grant usage on schema private to authenticated;
create or replace function private.is_team_member(p_team_id uuid)
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
select exists (
select 1
from public.team_members m
where m.team_id = p_team_id
and m.user_id = (select auth.uid())
);
$$;
revoke execute on function private.is_team_member(uuid) from public, anon;
grant execute on function private.is_team_member(uuid) to authenticated;
create policy "documents: team members can read"
on public.documents for select
to authenticated
using ( (select private.is_team_member(team_id)) );Roly
Roly čítajte z app_metadata, ktoré môže nastaviť iba server. Častá chyba v kóde od AI je kontrola user_metadata — prihlásení používatelia si však vlastné user metadata môžu upraviť z klienta, takže sa môžu sami urobiť administrátorom.
-- Bezpečné: app_metadata sa nastavuje iba na serveri
create policy "invoices: admins can read all"
on public.invoices for select
to authenticated
using ( (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );
-- Nebezpečné: user_metadata môže používateľ prepísať
-- using ( (auth.jwt() -> 'user_metadata' ->> 'role') = 'admin' )Keď politika nie je správny nástroj
RLS dobre odpovedá na otázku „smie sa tento používateľ dotknúť tohto riadku?“. Zle odpovedá na otázku „smie tento používateľ na bezplatnom pláne zverejniť štvrtý projekt a má sa pritom odoslať e-mail?“. Keď pravidlo závisí od kvót, stavu predplatného alebo od toho, že sa naraz mení niekoľko tabuliek, nechajte tabuľku uzamknutú a vystavte serverový endpoint alebo úzko vymedzenú funkciu v Postgrese, ktorá pravidlo overí a zápis vykoná. Politiky, ktoré sa snažia zakódovať celý biznisový proces, sú pomalé, ťažko sa testujú a ľahko v nich vznikne nenápadná chyba.
Aj storage buckety potrebujú politiky
Supabase Storage uchováva metadáta súborov v storage.objects a prístup riadia politiky RLS na tejto tabuľke. Verejný bucket vydá ktorýkoľvek súbor komukoľvek, kto má alebo uhádne URL, takže verejné buckety používajte iba na naozaj verejné súbory. Pre súbory používateľov použite súkromný bucket, konvenciu priečinka pre každého používateľa a na sťahovanie podpísané URL.
-- Súbory sú uložené v documents/<user-id>/<filename>
create policy "documents bucket: owners can read"
on storage.objects for select
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "documents bucket: owners can upload"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "documents bucket: owners can delete"
on storage.objects for delete
to authenticated
using (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid())::text
);Na každom buckete nastavte aj zoznam povolených MIME typov a limit veľkosti súboru. Bez nich sa dá formulár na nahrávanie avatarov zneužiť na hosťovanie ľubovoľných súborov na vašej doméne.
Časté chyby v RLS v kóde vygenerovanom AI
Za väčšinou z nich je rovnaký vzorec: AI narazila na chybu oprávnení a odstránila ju namiesto toho, aby ju opravila správne. Keď kontrolujete projekt na Supabase postavený s AI, hľadajte najprv tieto veci.
- `using (true)` alebo `with check (true)` na tabuľkách s dátami používateľov. Niekedy pridané „dočasne“, aby sa odblokovala nejaká funkcia.
- `to public` alebo žiadna rola pri politikách na zápis, takže anonymní návštevníci môžu vkladať alebo upravovať riadky.
- Politiky typu „ktorýkoľvek prihlásený používateľ“, napríklad
auth.role() = 'authenticated'na súkromných dátach: každý zákazník si prečíta riadky všetkých ostatných zákazníkov. - Politiky insert bez `with check` na vlastníctvo, takže používatelia môžu vytvárať riadky patriace niekomu inému.
- Views, ktoré obchádzajú RLS. Views štandardne bežia s právami vlastníka. Na Postgrese 15+ ich vytvárajte s
with (security_invoker = true), aby sa uplatnili politiky volajúceho. - Funkcie so `security definer` v `public`. Dajú sa volať cez API ako RPC a bežia so zvýšenými právami. Držte ich v súkromnej schéme alebo v nich overujte
auth.uid(). - Kľúč service role v klientskom kóde, aby sa obišla chyba politiky.
- Roly čítané z `user_metadata` namiesto
app_metadata.
Ako testovať politiky RLS
Politiky testujte tak, že sa vydávate za roly, ktoré na ne naozaj narazia: anon, používateľ A a používateľ B. Preklikanie aplikácie za seba dokazuje iba to, že vy vidíte svoje dáta. V SQL editore môžete prepnúť rolu a podvrhnúť JWT v rámci transakcie, ktorú potom vrátite späť:
begin;
-- vystupujte ako anonymný návštevník
set local role anon;
select count(*) from public.projects; -- očakávame 0
-- vystupujte ako používateľ A
set local role authenticated;
select set_config('request.jwt.claims',
'{"sub":"00000000-0000-0000-0000-00000000000a","role":"authenticated"}', true);
select count(*) from public.projects; -- očakávame iba riadky používateľa A
update public.projects set name = 'changed'
where owner_id = '00000000-0000-0000-0000-00000000000b'; -- očakávame 0 upravených riadkov
rollback;Potom testujte zvonka, tak ako útočník — iba s verejným kľúčom z vášho bundlu:
curl "https://<project-ref>.supabase.co/rest/v1/projects?select=*" \
-H "apikey: <anon-or-publishable-key>"
# Správne chránená tabuľka vráti []Keď sú politiky v poriadku, zafixujte ich automatickými testami. Supabase CLI spúšťa pgTAP testy príkazom supabase test db; pár asercií na tabuľku (anon nevidí nič, A neprečíta dáta B, A nemôže zmeniť vlastníka) zachytí ďalšiu úpravu od AI, ktorá politiku uvoľní.
- RLS zapnuté na každej tabuľke v každej vystavenej schéme.
- Žiadny kľúč
service_role/ secret v klientskom kóde, bundloch ani verejných repozitároch. - Samostatné politiky pre každú operáciu, každá s explicitnou rolou v
to. - Politiky insert a update používajú
with checkna vlastníctvo. - Privilegované stĺpce nie sú pre používateľov zapisovateľné.
- Roly pochádzajú z
app_metadata. - Views používajú
security_invoker; funkcie s definer právami sú v súkromnej schéme. - Súkromné buckety s politikami obmedzenými na priečinok, limitmi MIME typov a veľkosti.
- Politiky otestované ako anon, používateľ A a používateľ B — a pokryté pgTAP testami.
Ako vám môže pomôcť Backloo
Všetko vyššie zvládnete interne s SQL z tejto stránky. Ak chcete druhý pár očí, politiky RLS a storage sú kľúčovou súčasťou auditu v rámci našej záchrany AI aplikácií: zmapujeme každú tabuľku a bucket, otestujeme každú politiku ako anon aj ako konkurenční používatelia a odovzdáme vám migrácie a pgTAP testy, ktoré patria vám. Pri aplikáciách, ktoré prerástli dotazy priamo z prehliadača, presúvame citlivú logiku na server v rámci backendových systémov. Zvyšok zoznamu pred spustením — webhooky, premenné prostredia, monitoring — nájdete v článku prečo aplikácie z AI v produkcii padajú.
Otázky
Je anon kľúč Supabase tajný?
Nie. Anon (publishable) kľúč je určený na to, aby bol vo vašom frontende. Bezpečný je však iba vtedy, keď je na každej vystavenej tabuľke zapnuté Row Level Security so správnymi politikami, pretože práve RLS obmedzuje, čo tento kľúč dokáže.
Zapína Lovable Row Level Security automaticky?
Nepredpokladajte to. Vygenerované migrácie RLS zapnúť môžu, ale nemusia, a politiky môžu byť príliš voľné. Spustite dotaz z tohto článku, vypíšte si tabuľky bez RLS a každú politiku si skontrolujte sami.
Čo bolo CVE-2025-48757?
Záznam o zraniteľnosti, ktorý pokrýva aplikácie postavené v Lovable, ktorých tabuľky v Supabase nemali dostatočné Row Level Security. Dáta vo viac ako 170 aplikáciách tak boli prístupné komukoľvek s verejným kľúčom.
Spomaľujú politiky RLS moje dotazy?
Pri veľkých tabuľkách môžu, ak sú napísané naivne. Obaľte auth.uid() do poddotazu, indexujte stĺpce, podľa ktorých politiky filtrujú, a vyhľadávanie členstva presuňte do funkcie so security definer v súkromnej schéme.
Mám v Edge Functions používať kľúč service_role?
Iba vtedy, keď funkcia naozaj potrebuje obísť RLS, napríklad pri spracovaní platobného webhooku. Pri požiadavkách v mene používateľa vytvorte klienta s JWT používateľa, aby RLS stále platilo.