TL;DR
En Lovable-app er kun så sikker som sin Supabase-database, og “RLS er slået til” beviser meget lidt. Test med to brugere i to virksomheder, hold roller ude af brugermetadata, hold den hemmelige nøgle ude af browseren, og tjek rettigheder, views, storage, funktioner og rate limits hver for sig, for hver af dem går uden om dine policies på sin egen måde. Herunder er listen, jeg kører, prompten, du kan indsætte i Lovable eller Claude, og otte tjek, jeg tilføjede, efter en klients Lovable-app viste mig, hvad de første tolv overser.
Jeg er Denys, fullstack- og AI-ingeniør. Denne måned rettede jeg en klients app bygget med Lovable. Alt kørte på Supabase, som sædvanligt, og der var så mange sikkerhedsrettelser, at det føltes, som om appen var lavet af dem. Det her er listen, jeg nu gennemgår på hvert Lovable- eller Supabase-projekt, før jeg kalder kundedata beskyttet.
Er Lovable sikkert? Sikkerhedsproblemerne indtil nu
Platformen bygger og hoster din app, men om appen er sikker, afhænger mest af Supabase-databasen bag den, fordi appen læser og skriver direkte til databasen fra browseren. De offentlige optegnelser viser begge slags problemer:
| Hvornår | Hvad skete der | Kilde |
|---|---|---|
| Marts 2025 | CVE-2025-48757: en scanning af 1.645 Lovable-showcase-apps fandt 303 endpoints i 170 af dem, der udleverede data til alle med den offentlige nøgle, fordi RLS manglede eller var for løst | Matt Palmers offentliggørelse, maj 2025 |
| Februar 2026 | En Lovable-hostet app afslørede 18.697 brugerposter; et adgangstjek i dens funktioner lukkede anonyme besøgende ind og blokerede loggede brugere. Lovable sagde, at en del af koden ikke var genereret af Lovable | The Register, 27. februar 2026 |
| 3. februar – 20. april 2026 | En regression på platformen lod andre Lovable-brugere læse chathistorik og kildekode fra offentlige projekter | Lovables indlæg om hændelsen, 22. april 2026 |
De to første er huller på app-niveau, den slags tjekkene herunder fanger. Den tredje var Lovables egen, og Lovable rettede den; din del er at holde nøgler ude af chats og kode (tjek 3) og rotere dem, der var der. Lovable kører en sikkerhedsscanning før publicering, men det er op til dig at handle på den, og da CVE’en blev offentliggjort, tjekkede scanningen, at en policy fandtes, ikke at den var korrekt. Så “er Lovable sikkert” kommer ned til, om din database består tjekkene herunder.
Hvorfor er “RLS er slået til” ikke nok i en Lovable-app?
En Lovable-app taler med databasen direkte fra browseren. Der er ingen egen backend imellem: siden kalder Supabase Data API med en offentlig nøgle, og Postgres afgør, hvad der kommer tilbage (hvilke schemas det API leverer, er en indstilling for sig, som Supabase exposed schemas gennemgår). Så hver regel, der beskytter kundedata, skal ligge i databasen, og databasen har flere døre.
Row level security er én af dem. Rettigheder (grants) afgør, om en rolle overhovedet må røre en tabel, policies afgør, hvilke rækker, og tre ting springer policies helt over: den hemmelige nøgle, views oprettet uden security_invoker og SECURITY DEFINER-funktioner. Det er ikke teoretisk. Lovables egen sikkerhedsguide beder dig gennemgå de RLS-policies, den sætter op, og CVE-2025-48757 ovenfor viser, hvad der sker, når ingen gør det.
Prompten: indsæt den i Lovable eller Claude
Du kan indsætte den, som den er. Tjek bagefter resultatet selv: AI elsker at sige “færdig”, når det ikke er sandt, og en prompt, der får en fejl til at forsvinde, kan gøre det ved at give mere adgang.
Gennemgå dette projekt for sikkerhed, ret det, du finder, og fortæl mig, hvad du har ændret:
1. Test med to brugere i to virksomheder. Bruger A prøver at læse, redigere og slette Bs poster via id, API, søgning og eksport. Alt skal fejle.
2. RLS på hver tabel, policies tjekker rækkens ejer eller virksomhed. "Logget ind" er ikke nok.
3. Service role-nøglen aldrig i frontend. Edge functions, der bruger den, tjekker selv brugeren og filtrerer efter virksomhed, nøglen springer RLS over.
4. Roller, abonnement og virksomheds-id aldrig fra brugermetadata, brugere kan redigere dem. Gem dem i en tabel, som kun serveren skriver til.
5. Når en anmodning sender to id'er (projekt + opgave), tjek at opgaven hører til det projekt.
6. Træk alle tabelrettigheder tilbage fra anon og authenticated, giv kun det tilbage, som policies har brug for. RLS stopper ikke TRUNCATE. Hver migrering, der opretter en tabel, indeholder dens GRANT: fra 30. oktober 2026 giver Supabase ikke længere nye tabeller rettigheder automatisk, og appen får fejlen 42501.
7. Alle views med security_invoker = true, ellers springer de RLS over.
8. Storage-buckets: brugere læser og skriver kun deres egne filer, ingen offentlige buckets til private data.
9. SECURITY DEFINER-funktioner: NULL skal betyde afvis, ikke tillad.
10. Rate limits på login, tilmelding, nulstilling og OTP, gemt i databasen, ikke i hukommelsen.
11. Valider alt input på serveren.
12. Kør Supabase security advisor, ret hver advarsel.
De 12 tjek, og hvordan du beviser hvert af dem
1. To brugere i to virksomheder
Opret bruger A i virksomhed 1 og bruger B i virksomhed 2, hver med nogle få poster. Log ind som A og prøv at læse, redigere og slette Bs poster via id, gennem API’et, gennem søgning og gennem enhver eksport. Hvert forsøg skal returnere intet eller en fejl. Det er den ene test, der fanger det meste af det følgende, fordi den tester, hvad angriberen gør, ikke hvad du skrev.
Tilføj en tredje bruger, der tilhører begge virksomheder. En policy af formen “den, der kalder, er medlem af rækkens virksomhed” lader den bruger læse virksomhed 2’s rækker, mens vedkommende arbejder i virksomhed 1. I en anden Supabase-app returnerede min probe på sådan en bruger den anden virksomheds dokument, indtil forespørgslen også filtrerede på anmodningens virksomhed. Det skrev jeg op i en bruger i to orgs, som RLS ikke isolerede.
2. RLS på hver tabel, afgrænset til ejeren eller virksomheden
auth.uid() is not null betyder “enhver indlogget bruger”, hvilket på en offentlig tilmeldingsside betyder alle. Hver policy sammenligner rækkens ejer eller virksomhed med den, der kalder. List de tabeller, der stadig har RLS slået fra:
select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
3. Den hemmelige nøgle aldrig i frontend
Service role-nøglen og den nyere sb_secret_-nøgle omgår alle policies. Søg i den byggede JavaScript efter begge, og roter enhver nøgle, der nogensinde har været udleveret. Edge functions, der bruger den, skal selv tjekke, hvem brugeren er, og filtrere efter virksomhed, for med den nøgle gør intet andet det.
4. Roller, abonnement og virksomheds-id aldrig fra brugermetadata
raw_user_meta_data er det, klienten sendte til signUp, og brugere kan ændre det bagefter. I en anden app læste new-user-triggeren den inviterede rolle derfra, så enhver, der tilmeldte sig, kunne sende ejerrollen med og blive Owner. Gem roller, abonnementer og virksomheds-id’er i en tabel, som kun serveren skriver til, eller i app_metadata. Hele historien: en rolle taget fra tilmeldingsmetadata.
5. To id’er i én anmodning: tjek, at de hører sammen
Når en anmodning bærer et projekt-id og et opgave-id, er det ikke nok at autorisere projektet. En bruger med rettigheder på projekt A sender A plus et sektions-id fra projekt B, og rækken havner i B. RLS fanger det ikke, fordi insert stempler angriberens eget projekt-id, som netop er det, policyen tjekker. Tjek, at barnet hører til forælderen, før du skriver.
6. Rettigheder: træk tilbage, giv så kun det nødvendige tilbage
Rettigheder og policies er to adskilte lag. At tilføje policies tager ikke en rettighed tilbage, og derfor siger Supabases RLS-guide, at du eksplicit skal trække tilbage og give tilbage. I et andet projekt havde supabase/postgres 17.6.1-imaget givet anon og authenticated TRUNCATE, REFERENCES, TRIGGER og MAINTAIN på hver tabel i public, og ingen policy gælder for TRUNCATE. Data API’et har intet TRUNCATE-endpoint, så det er ikke en sletning med én anmodning, men ingen klientrolle har brug for det, og enhver funktion, der kører SQL som den rolle, arver det.
revoke all on public.orders from anon, authenticated;
grant select, insert, update on public.orders to authenticated;
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by table_name;
Én dato ændrer dette tjek. Supabase holder op med at give nye tabeller rettigheder automatisk: nye projekter siden 30. maj 2026 og alle eksisterende projekter fra 30. oktober 2026, også dem, Lovable har oprettet. Derefter er en tabel, som Lovable eller en migrering opretter uden en eksplicit GRANT, usynlig for appen: anmodningen fejler med 42501, og den funktion, der bruger tabellen, går i stykker. Så hver migrering, der opretter en tabel, bærer nu sine rettigheder, ved siden af enable row level security og policies.
To ting forbliver ens. Eksisterende tabeller beholder de rettigheder, de har, så det er stadig dit job at trække tilbage, hvad du ikke har brug for. Og ændringen tager kun select, insert, update og delete tilbage: i Lovable-projektet fulgte nye tabeller med de nye standarder stadig med TRUNCATE, REFERENCES, TRIGGER og MAINTAIN for anon og authenticated. Træk først alt tilbage og giv så tilbage er stadig den sikre rækkefølge.
7. Hvert view med security_invoker = true
Et view kører med ejerens rettigheder, og i Supabase er ejeren som regel postgres, så et view over en beskyttet tabel udleverer hver række, som policies tilbageholder. På Postgres 15 og nyere:
alter view public.customer_summary set (security_invoker = true);
8. Storage: kun egne filer, ingen offentlig bucket til private data
En offentlig bucket leverer enhver fil til alle med URL’en. Private filer hører i en privat bucket med policies på storage.objects, der tjekker ejeren eller virksomheden, som regel ud fra første mappe i stien. Efter en flytning af et projekt: tæl de aktive storage-policies. I en app fejlede hver upload efter en flytning, fordi policy-migreringen aldrig kørte, og tabellen havde RLS slået til uden en eneste policy.
9. SECURITY DEFINER-funktioner: NULL betyder afvis
I PL/pgSQL kaster if not (has_permission or assignee_id = auth.uid()) ingen fejl, når assignee_id er null: hele betingelsen er null, og if springer den over. Det samme udtryk er sikkert i en policy, som behandler null som afvis. I en app lod det en rolle med alle rettigheder slået fra handle på alle 3683 opgaver, fordi ingen havde en ansvarlig. Pak betingelsen ind i coalesce(…, false). Detaljer: en NULL i PL/pgSQL IF NOT sprang tjekket over.
10. Rate limits gemt i databasen
Begræns login, tilmelding, nulstilling af adgangskode og OTP, og hold tælleren i Postgres. I Lovable-appen lå grænserne i en variabel inde i edge function’en. Jeg probede det hostede projekt den 17. oktober 2026: hver anmodning kørte i en ny isolate, så tælleren i hukommelsen var altid 1, og grænsen kunne aldrig udløses. Lokal supabase functions serve genbruger isolate’n, så den samme kode bestod alle lokale tests.
Nøglen betyder lige så meget som lageret. På den hostede platform er det sidste X-Forwarded-For-led Supabases egen proxy, så at bruge det som nøgle lægger alle kaldere i én spand, og x-client-ip sættes af klienten. I min probe bar cf-connecting-ip den rigtige klient. Rettelsen var en SECURITY DEFINER-funktion claim_rate_limit(bucket, key, max, window), som kun service role kan udføre, serialiseret med en advisory lock, og som gemmer en hash af IP-adressen eller bruger-id’et frem for den rå værdi.
11. Valider alt input på serveren
Uanset hvad formularen tjekker, accepterer API’et alt, hvad klienten sender. Længder, formater, enums og ejerskab tjekkes i databasen (constraints, triggers) eller i edge function’en, aldrig kun i React.
12. Supabase Security Advisor, hver advarsel
Åbn Advisors, derefter Security Advisor, og ret hvert fund. Den markerer tabeller uden RLS, views der springer RLS over, policies der læser brugermetadata, åbne using (true)-policies og funktioner med en foranderlig search_path. Den kan ikke se tjek 1, 5, 9 eller 10, fordi de handler om logik, ikke konfiguration.
Hvad lærte Lovable-appen mig, som de 12 overser?
Disse kom ud af klientens app og dens gennemgange. Ingen af dem dukker op i en scanner.
- Felter, klienten ikke må sætte. Klienten sendte selv
status: 'approved', som sprang moderationen over. En UPDATE-policy, hviswith checker løsere end densusing, lader også en bruger flytte en række til en anden ejer. En policy kan ikke beregne en værdi, så enBEFORE INSERT OR UPDATE-trigger udlederstatus, rolle, pris og virksomhed på ny ud fra serverens egne data og ignorerer det, klienten sendte. - Hver skrivevej, ikke kun den primære insert. Efter at den primære insert var rettet, sad den samme omgåelse i vedhæftninger, kommentarer, gendannelse af et blødt slettet element og tællere. List hver skriver til en beskyttet kolonne, før du retter den første.
- Tilladelseslister, ikke afvisningslister. Forfalskede poster tog fem gennemgangsrunder, fordi hver rettelse blokerede én kombination og lod en søskende stå åben. En trigger, der kun tillader de listede kombinationer af type og status, lukkede hele klassen på én gang.
- Funktioner er også et API. Hver funktion i
publickan kaldes via/rpcaf enhver rolle med EXECUTE, og RLS gælder ikke for funktioner. Giv hverSECURITY DEFINER-funktionset search_path = '', træk EXECUTE tilbage frapublic,anonogauthenticated, og giv det kun til den rolle, der har brug for det. - At trække EXECUTE tilbage kan ødelægge offentlige sider. Postgres tjekker EXECUTE på hver funktion, som hver policy for rollen bruger, selv i en gren, der aldrig ville køre. Da
anonmistede EXECUTE påis_admin(), fejlede hver anonym læsning med 42501. Policies kun for administratorer fårto authenticated, og offentlige læsninger får deres egen policy uden administratorgrenen. - Tilmelding med e-mailbekræftelse slået til. Med bekræftelse slået til returnerer
signUpingen session, så enhver profilskrivning lige efter kører somanonog skriver i det stille ingenting. Lovable-projekter har ofte auto-confirm slået til, hvilket skjuler fejlen. Send felterne isignUp({ options: { data } }), lad new-user-triggeren validere og skrive dem, og behandl de metadata som upålidelige (tjek 4). - Tests, der består af den forkerte grund. Den lokale stack og det hostede projekt havde forskellige standardrettigheder, så “anon kan ikke indsætte” bestod lokalt, fordi rettigheden manglede, ikke fordi RLS virkede. En negativ test betyder først noget, når den positive rettighed findes. Sammenlign
pg_default_acllokalt og i produktion. - Den levende database, ikke migreringsfilerne. Efter en migrering eller en flytning af projektet: sammenlign, hvad der faktisk er live:
pg_policies,pg_get_functiondeffor hver spærre og rettighederne ovenfor. I en anden app kopierede en ny migrering en gammel funktionskrop og fortrød i det stille en senere rettelse.
Er en Lovable-app GDPR-kompatibel?
Lovable gør ikke en app kompatibel eller ukompatibel; det afgøres af, hvad appen gør med personoplysninger, og en sikkerhedsgennemgang svarer ikke på det. Her er de seks huller, jeg tjekker i en Lovable Cloud-app, hver med løsningen:
- List alle databehandlere. Lovable Cloud kører databasen, auth, lagring og funktioner på Supabase, Lovable hoster sitet og kan sende login-mails, og Lovable AI Gateway sender tekst videre til en modeludbyder. Tilføj alle mail-, AI- eller scraping-tjenester, dine funktioner kalder. Hver tjeneste, der modtager personoplysninger, kræver en databehandleraftale, og en uden for EU kræver også et overførselsgrundlag. Hostingregionen fremgår ikke af koden, så spørg efter den.
- Slet en bruger, der har indhold. Fremmednøgler fra det, brugeren har skrevet, kan blokere sletningen, og så kan en anmodning om sletning ikke gennemføres. Lad de nøgler kaskadere eller blive sat til null, tilføj en sletningstrigger, der også fjerner brugerens mail-logs og login-tokens, og behold en databasetest, der sletter sådan en bruger.
- Sæt en opbevaringsgrænse. Mail-logs, visningssporing, køer med fejlede mails og blødt slettede rækker gemmer ofte personoplysninger uden slutdato. Et planlagt job, der sletter blødt slettede rækker endeligt efter en fast periode og fjerner deres filer, lukker det meste.
- Mailsporing kun med samtykke. At registrere åbninger og klik pr. modtager kræver samtykke efter ePrivacy-reglerne: et samtykkeflag, slået fra som standard, med tidsstemplet sat af serveren, ikke af klienten.
- Hold adresser ude af funktionslogs. Logs fra edge functions bliver ikke renset, så en
console.logmed en modtageradresse efterlader adressen der. Log id’er og antal. - Slå e-mailbekræftelse til. Med auto-bekræftelse kan alle tilmelde sig med en adresse, de ikke ejer, og bringe en rigtig persons adresse ind i appen. Punkt 6 i listen ovenfor dækker tilmeldingssiden.
Sådan tjekker du din egen app: søg efter API-nøgler i funktionerne for at liste tjenesterne, slet en testbruger med indhold, søg efter @ i funktionslogs, og bed hver udbyder om databehandleraftalen. Det her er ikke juridisk rådgivning.
Hvordan forhindrer du, at det kommer tilbage?
Hvert tjek ovenfor er en forespørgsel eller en test, så gem dem i repoet og kør dem før hver udrulning. Supabases pgTAP-tests dækker rettigheder, policies og funktioner; to-virksomheder-testen hører ved siden af dem. Hvis din app har roller og en rettighedsskærm, er en test pr. policy heller ikke nok: jeg beviser hvert rettighedsfelt mod en kopi af produktion, beskrevet i sådan tester du Supabase RLS, hvor 30 af 87 felter viste sig ikke at gøre noget. Det gratis Supabase sikkerhedstjek kører 46 OWASP Top 10:2025-regler mod et projekts roller, rettigheder, policies og auth-indstillinger, så brug det til første gennemgang; to-virksomheder-testen og alt om din brugerflade er stadig manuelt.
Hvad listen ikke dækker: betalinger og webhooks, hemmeligheder i din Git-historik, sikkerhedsheadere på hosten, backups. De betyder noget, men det er en anden tjekliste.
Hvis du har arvet en Lovable- eller Supabase-app og ikke er sikker på, hvilke af disse den består, er denne gennemgang der, hvor en projektredning starter. Casen om tidsregistrering for et revisionsfirma viser, hvordan rolletjekkene ser ud i en produktionsapp.
Fandt du et hul, som listen overser?
Det vil jeg gerne høre. Jeg udveksler gerne erfaringer om at sikre Lovable- og Supabase-apps.
Kontakt mig