Aplikace vypadala připravená k produkčnímu nasazení. Čisté uživatelské rozhraní, fungující integrace Stripe, propracovaný proces registrace. Byla uvedena na Product Hunt, získala přes 200 uživatelů během prvního týdne a již generovala MRR. Zakladatel ji postavil za 48 hodin během hackathonového víkendu s použitím Next.js a Supabase, což je preferovaný stack pro každého, kdo chce rychle uvádět produkty na trh.
Osm minut po zahájení skenování Penetrify jsme označili Kritické zjištění: záznam profilu každého uživatele – jméno, e-mail, metadata účtu – byl čitelný jakýmkoli jiným ověřeným uživatelem prostřednictvím automaticky generovaného REST API Supabase. Nebyla vyžadována žádná sofistikovanost útočníka. Stačilo vyměnit své ID uživatele za jiné v URL adrese.
Toto je příběh o tom, jak k tomu došlo, proč je to běžnější, než si kdokoli chce připustit, a jak přesně vypadala oprava.
Co je Supabase Row Level Security — A co se stane, když je vypnutá
Supabase je postaveno na PostgreSQL a zpřístupňuje vaše databázové tabulky přímo přes REST API (prostřednictvím PostgREST) a klientskou knihovnu JavaScriptu. To je skutečně silné pro rychlý vývoj: můžete dotazovat svou databázi z frontendu, aniž byste museli psát jedinou API routu.
Mechanismus, který brání uživatelům číst data ostatních, se nazývá Row Level Security (RLS). RLS je funkce PostgreSQL, která vám umožňuje definovat zásady řídící, které řádky může daný databázový uživatel SELECT, INSERT, UPDATE nebo DELETE. V aplikaci Supabase byste typicky napsali zásadu jako:
-- Only allow users to read their own profile
CREATE POLICY "Users can view own profile"
ON profiles
FOR SELECT
USING (auth.uid() = user_id);
Když je RLS povoleno a je zavedena taková zásada, dotaz na profil jiného uživatele vrátí nula řádků – databáze vynucuje kontrolu přístupu na úrovni řádků, než se jakákoli data dostanou do aplikace.
Když je RLS zakázáno nebo neexistuje žádná zásada, tabulka se chová, jako by každý řádek byl veřejný pro jakýkoli ověřený požadavek. REST API Supabase, dostupné na https://[project].supabase.co/rest/v1/profiles, vrátí všechny řádky v tabulce – data každého uživatele – každému, kdo má platný JWT z daného projektu.
Co jsme zjistili: Pět zjištění, dvě z nich kritická
Skenování proběhlo za osm minut v rychlém režimu proti živé aplikaci. Zde jsou zjištění seřazená podle závažnosti:
KRITICKÉ — Supabase RLS není povoleno na tabulce profilů
Tabulka profilů měla RLS zakázáno. Jakýkoli ověřený uživatel ji mohl dotazovat prostřednictvím REST endpointu Supabase a získat všechny uživatelské záznamy. Požadavek, který to demonstroval:
GET /rest/v1/profiles?select=*
Authorization: Bearer [any valid user JWT]
HTTP/1.1 200 OK
[
{"id": "uuid-1", "email": "user1@example.com", "full_name": "...", ...},
{"id": "uuid-2", "email": "user2@example.com", "full_name": "...", ...},
... (all 200+ user records)
]
Podle GDPR se jedná o únik osobních údajů, který se týká každého registrovaného uživatele. Anonymní klíč Supabase je záměrně vložen do frontendového JavaScriptového balíčku – je bezpečné ho veřejně vystavit, pokud je RLS správně nakonfigurováno, protože samotný klíč neposkytuje žádný zvýšený přístup. Bez RLS se stává hlavním klíčem ke všem uživatelským datům.
KRITICKÉ — Ověření e-mailu není vynuceno na chráněných koncových bodech
Supabase ve výchozím nastavení umožňuje registrace pomocí e-mailu a hesla a odesílá potvrzovací e-mail k ověření adresy. Nicméně, backendové API routy aplikace nekontrolovaly, zda si požadující uživatel potvrdil svůj e-mail před udělením přístupu k chráněné funkcionalitě.
Útočník se mohl zaregistrovat s victim@example.com (e-mailová adresa skutečného uživatele), zcela přeskočit ověření e-mailu a okamžitě přistupovat k chráněným API cestám aplikace, jako by mu tato e-mailová adresa patřila. V kombinaci s problémem RLS to znamenalo, že neověřený útočník mohl exfiltrovat celou uživatelskou databázi vytvořením jednorázového účtu s jakoukoli e-mailovou adresou.
STŘEDNÍ — IDOR na /api/export
Aplikace měla exportní koncový bod, který přijímal ID uživatele jako parametr dotazu:
GET /api/export?userId=abc123
Na straně serveru nebyla provedena žádná kontrola vlastnictví. Jakýkoli ověřený uživatel mohl exportovat data jakéhokoli jiného uživatele nahrazením vlastního ID ID cíle. ID uživatelů byla odhalena v API odpovědích napříč celou aplikací, což činilo enumeraci triviální.
STŘEDNÍ — Žádné omezení rychlosti na přihlašovacím koncovém bodu
Přihlašovací koncový bod přijímal autentizační požadavky přibližně 500 požadavků za sekundu bez omezování nebo uzamčení. Útok typu credential stuffing proti uživatelské základně aplikace by nenarazil na žádné překážky.
STŘEDNÍ — JWT tokeny uložené v localStorage bez rotace
JWT tokeny Supabase byly uloženy v localStorage, přístupné jakémukoli JavaScriptu běžícímu na stránce. Při změnách oprávnění nedocházelo k rotaci tokenů. Úspěšný XSS útok na jakékoli stránce by útočníkovi poskytl perzistentní, platnou relaci.
Proč se to děje: Past výchozích nastavení Supabase
Toto není příběh o nedbalém vývojáři. Je to příběh o výchozích nastaveních.
Když vytvoříte novou tabulku v Supabase, RLS je ve výchozím nastavení zakázáno. Vlastní dokumentace Supabase to jasně popisuje a doporučuje jeho povolení, ale příklady rychlého startu – ty, které vývojáři skutečně následují, když staví ve 2 ráno během hackathonu – často RLS pro stručnost vynechávají. Vidíte funkční dotaz v příkladu, zkopírujete vzor, nasadíte to.
Dashboard Supabase zobrazuje žlutou varovnou ikonu u tabulek bez RLS. Je snadné ji přehlédnout, když se soustředíte na uživatelské rozhraní, integraci plateb, proces onboardingu. Neexistuje žádná chyba, žádné selhání za běhu, žádný zjevný symptom. Vše funguje perfektně. Uživatelé se mohou registrovat, přihlašovat a používat aplikaci. Zranitelnost je zcela tichá.
Nejnebezpečnější bezpečnostní chyby jsou ty, které vypadají přesně jako správné chování.
Tento vzor se objevuje téměř v každé aplikaci podporované Supabase, kterou skenujeme a která byla postavena s důrazem na rychlost. Ne proto, že by se vývojáři nestarali o bezpečnost, ale proto, že časový tlak hackathonu nebo spouštěcího sprintu nenechává prostor pro přečtení každé sekce dokumentace.
Oprava: Dvě hodiny, žádné přepisování kódu
Zakladatel opravil všech pět zjištění za méně než dvě hodiny téhož večera, kdy zpráva dorazila. Zde je přesně to, co bylo uděláno:
RLS — 15 minut v dashboardu Supabase
Povolení RLS a přidání odpovídajících politik nevyžadovalo žádné změny kódu. V dashboardu Supabase, pod Editor tabulek → profily → RLS politiky:
-- Enable RLS on the table (one toggle in the dashboard)
-- Then add policies:
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = user_id);
-- For admin access (if needed):
CREATE POLICY "Service role has full access"
ON profiles
USING (auth.role() = 'service_role');
Po povolení RLS a přidání těchto politik hromadný dotaz vrátil nula řádků pro jakéhokoli ověřeného uživatele, který požadoval data, jež mu nepatřila.
Ověření e-mailu — jedna kontrola v middleware
API cesty Next.js již četly uživatelský objekt Supabase z JWT při každém požadavku. Přidání vynucení ověření e-mailu byla kontrola na jednom řádku v autentizačním middleware:
const { data: { user } } = await supabase.auth.getUser()
if (!user?.email_confirmed_at) {
return res.status(403).json({ error: 'Email verification required' })
}
IDOR na /api/export — jednořádková oprava v middleware
Oprava spočívala v nahrazení uživatelem dodaného parametru userId vlastním ID ověřeného uživatele, extrahovaným z ověřeného JWT:
// Before (vulnerable)
const userId = req.query.userId
// After (fixed)
const { data: { user } } = await supabase.auth.getUser()
const userId = user.id // always the authenticated user — can't be spoofed
Omezení rychlosti požadavků — vestavěné omezení rychlosti požadavků Vercelu
Aplikace byla hostována na Vercelu. Přidání omezení rychlosti požadavků na přihlašovací endpoint vyžadovalo přidání balíčku @upstash/ratelimit a obalení routy — přibližně 20 řádků kódu. Zakladatel tuto opravu nasadil následující ráno.
Odhalení podle GDPR
Problém s RLS se týkal všech více než 200 registrovaných uživatelů. Jejich kompletní profilové záznamy — e-mailové adresy, zobrazovaná jména a jakákoli další pole uložená v tabulce profilů — byly čitelné jakýmkoli jiným ověřeným uživatelem po celou dobu, co byla aplikace v provozu.
Podle článku 33 GDPR musí být narušení osobních údajů, které „pravděpodobně povede k riziku pro práva a svobody fyzických osob“, nahlášeno příslušnému dozorovému úřadu do 72 hodin od okamžiku, kdy se o něm dozvíte. Zda toto konkrétní narušení překročilo tuto hranici, by záviselo na citlivosti uložených dat a na tom, kolik uživatelských dat bylo skutečně získáno — ale okno pro odhalení bylo otevřené.
Zakladatel změnil všechny citlivé konfigurační hodnoty, povolil RLS a opravil zbývající problémy ještě týž večer. V logech Supabase nebyly nalezeny žádné důkazy neoprávněného přístupu. Problém byl vyřešen dříve, než se stal incidentem.
Jak zkontrolovat vlastní aplikaci Supabase
Pokud provozujete aplikaci založenou na Supabase, zde je rychlý samoaudit, který můžete provést za pět minut:
- Otevřete panel Supabase → Table Editor. Jakákoli tabulka bez zelené ikony štítu má RLS zakázáno. Pokud tato tabulka obsahuje uživatelská data, je pravděpodobně odhalena.
- Otevřete vývojářské nástroje prohlížeče ve vaší produkční aplikaci → záložka Network → filtrujte podle URL vašeho projektu Supabase. Hledejte požadavky na
/rest/v1/[tablename]?select=*. Pokud vidíte požadavky vracející mnoho záznamů, zkontrolujte, zda by měly být omezeny na ověřeného uživatele. - Zkontrolujte tok ověřování e-mailu. Zaregistrujte si nový účet, přeskočte potvrzovací e-mail a pokuste se přímo přistoupit k chráněným částem vaší aplikace. Pokud se dostanete k chráněné funkcionalitě bez potvrzení e-mailu, váš backend nevynucuje ověření.
- Zkontrolujte své exportní/stahovací endpointy. Jakýkoli endpoint, který přijímá ID uživatele jako parametr, by jej měl křížově zkontrolovat s JWT ověřeného uživatele. Pokud lze parametr volně nahradit pro přístup k datům jiného uživatele, jedná se o IDOR.
Tyto čtyři kontroly zaberou méně než deset minut a pokrývají nejčastější bezpečnostní mezery Supabase, které vidíme v produkčních aplikacích.
Obecnější vzorec
Tato případová studie není neobvyklá. Je reprezentativní pro to, co nacházíme u většiny aplikací založených na Supabase, které byly vytvořeny během hackathonu, rychlého spuštění, nebo samostatným zakladatelem bez bezpečnostního zázemí.
Kombinace rychlých nástrojů, vynikajícího vývojářského zážitku a veřejných výchozích nastavení, která upřednostňují snadné použití před bezpečností, znamená, že mnoho aplikací je dodáváno s těmito mezerami. Tyto mezery jsou opravitelné — často během několika minut, zřídka vyžadují něco víc než SQL politiku nebo jednořádkovou kontrolu v middleware. Nejtěžší je vůbec vědět, že existují.
Tým Supabase odvedl značnou práci, aby RLS bylo výraznější v ovládacím panelu a dokumentaci. Ale propast mezi „dokumentace říká, že to máte povolit“ a „každá produkční aplikace to má skutečně povolené“ zůstává široká. Dokud se automatizované bezpečnostní testování nestane součástí standardního kontrolního seznamu pro spuštění, tento vzorec se bude stále objevovat.
