CVE-2025-48757 to luka w Lovable oceniona jako krytyczna (CVSS 9.3), która ujawniła dane ponad 170 aplikacji. Problem: każdy, bez logowania, mógł czytać, a w wielu przypadkach także modyfikować, dane w Twojej bazie: imiona i nazwiska, e-maile, klucze API firm trzecich, nawet status płatności. Przyczyną nie jest ukryty błąd Lovable, lecz wzorzec wspólny dla niemal wszystkich aplikacji z vibe codingu: powierzenie całego bezpieczeństwa bazy danych politykom RLS w Supabase… i pozostawienie ich niekompletnymi. Ten poradnik wyjaśnia, co dokładnie się stało, jak w pięć minut sprawdzić, czy Twoja aplikacja ma tę samą dziurę, jak ją zamknąć konkretnym SQL-em i co zrobić, jeśli dane już wyciekły.
Co naprawdę się stało
Lovable buduje aplikacje, w których przeglądarka rozmawia bezpośrednio z bazą danych (Supabase) przez wywołania REST, używając publicznego klucza anon. To solidny projekt, pod warunkiem że każda tabela ma polityki Row-Level Security (RLS) decydujące, które wiersze każdy użytkownik może czytać lub zmieniać. Luka pojawiła się, bo wiele projektów trafiło na produkcję z tabelami bez RLS lub z niewystarczającym RLS. Atakującemu wystarczyło zmodyfikować żądanie REST, zmienić filtr w URL-u, by poprosić o „wszystkie wiersze" tabeli, która powinna być chroniona.
Żeby zrozumieć, dlaczego wystarczy edycja URL-a, trzeba wiedzieć, jak Supabase działa pod maską. Przed Twoją bazą Postgres stoi warstwa o nazwie PostgREST, która zamienia każdą tabelę w endpoint HTTP: GET /rest/v1/customers?select=* zwraca wiersze z tabeli customers. Klucz anon nie jest sekretem: jest w JavaScripcie Twojej aplikacji i każdy może go zobaczyć. Jedyne, co decyduje, które wiersze wychodzą z tego endpointu, to polityki RLS tabeli. Bez nich odpowiedzią jest cała tabela.
Lukę zgłosili w marcu 2025 Matt Palmer i Kody Low, a niezależnie Danial Asaria; publiczne ujawnienie nastąpiło 29 maja 2025 (analiza techniczna, wpis CVE). Zagrożony może być każdy projekt Lovable z bazą utworzoną 15 kwietnia 2025 lub wcześniej.
Oś czasu w skrócie
- Marzec 2025: pierwsze prywatne zgłoszenia do Lovable; znalezione zostają opublikowane aplikacje z tabelami czytelnymi bez sesji.
- 15 kwietnia 2025: data graniczna, którą Lovable podaje dla zagrożonych projektów; aplikacje generowane później zawierają zmiany w sposobie tworzenia tabel.
- 29 maja 2025: publiczne ujawnienie i nadanie CVE z oceną krytyczną.
- Później: Lovable dodaje ostrzeżenia bezpieczeństwa w edytorze, ale polityki pozostają odpowiedzialnością tego, kto publikuje aplikację.
Dlaczego dotyczy Cię to, nawet jeśli nie używasz Lovable
To nie jest problem wyłącznie Lovable. Każda aplikacja, która rozmawia z Supabase bezpośrednio z przeglądarki, niezależnie od tego, czy zbudowałeś ją w Bolt, v0, Cursorze, Replit czy ręcznie, polega na poprawnym RLS na każdej tabeli. Aplikacja wygląda idealnie, logowanie działa, ale prawdziwe bezpieczeństwo mieszka w bazie danych, nie w interfejsie. Ukrycie przycisku we frontendzie niczego nie chroni: żądanie REST wciąż tam jest dla każdego, kto wie, gdzie szukać.
To samo dotyczy Firebase z regułami w „trybie testowym", PocketBase bez reguł kolekcji czy dowolnego backendu jako usługi, który przeglądarka wywołuje bezpośrednio. Wzorzec jest zawsze ten sam: AI, które wygenerowało Twoją aplikację, wie, jak sprawić, by działała, ale rzadko pisze reguły, które powstrzymują ją przed działaniem dla tych, którzy nie powinni.
Jak sprawdzić, czy Twoja aplikacja jest podatna (5 minut)
Test 1: żądanie w trybie incognito
- Otwórz DevTools (F12) → zakładka Network podczas korzystania z aplikacji i obserwuj wywołania do
*.supabase.co/rest/v1/. Skopiuj jeden z tych URL-i w całości (apikeypodróżuje w nagłówku; w URL-u zobaczysz tabelę i filtr). - Otwórz go w oknie incognito, bez sesji. Jeśli zwraca dane, dostaje je też każdy odwiedzający.
- Spróbuj poprosić o więcej: jeśli wywołanie zwraca Twoje dane, usuń filtr z URL-a (część
?user_id=eq.…). Jeśli wydaje wiersze innych użytkowników, Twoje RLS nie działa.
Test 2: panel Supabase
Otwórz projekt → Table Editor. Każda tabela bez RLS ma czerwoną etykietę „RLS disabled". Potem przejdź do Authentication → Policies: tabela z włączonym RLS, ale bez polityki odmawia wszystkiego (dobrze dla danych, ale Twoja aplikacja przestanie działać), a tabela z polityką using (true) jest tak otwarta, jakby RLS było wyłączone. Panel ma też Security Advisor (w sekcji Advisors), który wypisuje dokładnie te tabele.
Test 3: jedno zapytanie SQL
W SQL Editorze panelu to zapytanie wypisuje tabele w schemacie public (tym, który wystawia API) bez włączonego RLS:
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 not c.relrowsecurity;
Jeśli zwróci jakikolwiek wiersz z prawdziwymi danymi, ta tabela jest w tej chwili otwarta przez API.
Jak to naprawić, krok po kroku
1. Włącz RLS na każdej tabeli
Nie tylko na tych „ważnych". Zasada brzmi odmawiaj domyślnie: bez polityki, która na to pozwala, nikt nie wchodzi. Dla każdej tabeli w schemacie public:
alter table public.customers enable row level security;
Gdy to zrobisz, Twoja aplikacja przestanie widzieć dane, dopóki nie napiszesz polityk. Tak ma być: drzwi są teraz zamknięte i zaraz otworzysz tylko to, co potrzebne.
2. Napisz jawne polityki dla każdej operacji
Najczęstszy wzorzec to „każdy użytkownik widzi i edytuje tylko własne wiersze", porównując auth.uid() z kolumną przechowującą właściciela. Dla tabeli customers z kolumną user_id:
create policy "customers: czytaj własne"
on public.customers for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "customers: wstawiaj własne"
on public.customers for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "customers: aktualizuj własne"
on public.customers for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "customers: usuwaj własne"
on public.customers for delete
to authenticated
using ((select auth.uid()) = user_id);
Trzy szczegóły robią różnicę: to authenticated zostawia anonimowych odwiedzających za drzwiami; with check uniemożliwia komukolwiek wstawianie lub edycję wierszy w cudzym imieniu (bez niego użytkownik mógłby tworzyć dane z cudzym user_id); a opakowanie auth.uid() w select sprawia, że Postgres oblicza je raz na zapytanie zamiast raz na wiersz, co na dużych tabelach wielokrotnie poprawia wydajność. Dodaj indeks na user_id, jeśli go nie masz.
3. Nigdy nie używaj klucza service_role we frontendzie
Całkowicie omija RLS: z nim żadna z polityk, które właśnie napisałeś, nie istnieje. Ten klucz żyje tylko na serwerze (Edge Functions, Twój własny backend, worker). Jeśli kiedykolwiek był w kodzie klienta, traktuj go jako ujawniony i zrotuj go w panelu. Supabase oferuje teraz także nowsze klucze (sb_publishable_… do przeglądarki i sb_secret_… na serwer), które czynią ten podział bardziej oczywistym.
4. Przetestuj ponownie
Powtórz test incognito: żądanie bez sesji musi zwrócić pustą listę albo błąd 401. Potem zaloguj się na dwa różne konta i potwierdź, że żadne nie widzi danych drugiego. Jeśli masz testy automatyczne, to jest przypadek, który na jeden zasługuje.
Częste błędy podczas naprawy
- Polityki z
using (true). Asystenci AI często je podpowiadają, „żeby działało". Zostawiają tabelę tak otwartą, jakby RLS w ogóle nie było. - Zapomnienie o
with check. Chroni odczyty, ale pozwala na zapisy w cudzym imieniu. - Tabele „katalogowe" z danymi osobowymi. Publiczna tabela profili, która ma pokazywać imiona, kończy ujawniając e-maile lub numery telefonów. Wystawiaj tylko potrzebne kolumny przez widok.
- Widoki omijające RLS. W Postgresie widok działa z uprawnieniami tego, kto go utworzył. Utworzony z panelu może zwracać wiersze, których polityki tabeli bazowej by zabroniły. Twórz widoki z
security_invoker = true(Postgres 15 lub nowszy) albo sprawdzaj je po kolei. - Storage bez polityk. Buckety mają własne reguły. Publiczny bucket serwuje każdy plik każdemu, kto ma URL; dokumenty prywatne należą do prywatnych bucketów z politykami per użytkownik.
- Naprawianie tylko produkcji. Jeśli trzymasz projekt deweloperski ze skopiowanymi danymi, jest tak samo wystawiony.
Jeśli dane już wyciekły
Najpierw zamknij drzwi (kroki powyżej). Potem zrotuj każdy klucz, który leżał w czytelnych tabelach: klucze API firm trzecich, tokeny, webhooki. Przejrzyj logi API w panelu Supabase, żeby oszacować, co zostało odczytane i od kiedy. Jeśli dane obejmują informacje osobowe użytkowników z Europy, RODO wymaga oceny, czy trzeba powiadomić organ ochrony danych w ciągu 72 godzin, a w przypadkach wysokiego ryzyka także samych użytkowników. Udokumentuj, co znalazłeś i co zrobiłeś: zapytają Cię o to, jeśli ktoś zapuka do drzwi.
Prompt do naprawy z Twoim AI
Jeśli pracujesz z Cursorem, Claude'em albo czatem samego Lovable, ten prompt zwykle daje dobry wynik: „Wypisz wszystkie tabele w schemacie public mojego projektu Supabase. Dla każdej włącz Row-Level Security i napisz osobne polityki dla select, insert, update i delete ograniczone do roli authenticated, tak aby każdy użytkownik miał dostęp tylko do wierszy, których kolumna user_id równa się (select auth.uid()). Użyj with check przy insert i update. Powiedz mi, które tabele nie mają kolumny właściciela, żebym zdecydował o nich ręcznie. Nigdy nie używaj using (true)."
Jak sprawdza to Scanaris
Scanaris skanuje Twoją stronę tylko do odczytu i wykrywa odcisk palca tego wzorca: endpointy Supabase osiągalne z przeglądarki, liberalny CORS, klucze i tokeny wystawione w Twoim JavaScripcie oraz dane, które aplikacja serwuje bez sesji. Jeśli podasz connection string tylko do odczytu, audytuje też tabela po tabeli, czy RLS jest włączone i czy istnieją polityki. Wklej swój URL, a powie Ci, w kilka sekund i według wagi, co leży na widoku, zanim znajdzie to ktoś inny.
Najczęściej zadawane pytania
Czy Lovable już to naprawił?
Lovable zmienił sposób generowania tabel i dodał ostrzeżenia, ale aplikacja opublikowana przed 15 kwietnia 2025 zostaje taka, jaką ją zostawiłeś: polityki nie dodają się same. A w każdym projekcie, nowym czy starym, każda tabela utworzona później potrzebuje własnych.
Czy włączenie RLS zepsuje moją aplikację?
Włączenie bez polityk tak: wszystko jest odrzucane. To poprawne zachowanie. Napisz polityki z kroku 2, a aplikacja znów będzie działać, ale tylko dla tych, dla których powinna.
Czy mogę zabezpieczyć tabelę od strony frontendu?
Nie. Frontend to kod działający na komputerze odwiedzającego, który można dowolnie modyfikować. Jedyne, czego nie da się obejść, to reguły egzekwowane przez bazę danych.
A jeśli moja aplikacja nie ma użytkowników, tylko publiczną tabelę treści?
Wtedy publiczna polityka odczytu jest uzasadniona: for select to anon, authenticated using (true). Nie może natomiast istnieć otwarta polityka zapisu ani kolumny z danymi osobowymi w tej tabeli.