Hopp til innhold

Lovable-app sikkerhet: 12 sjekker, og 8 jeg la til

Oppdatert

Vis som MarkdownÅpne i ClaudeÅpne i ChatGPTÅpne i Perplexity

Spørsmålet kopieres også. Hvis chatten åpnes tom, lim det inn.

TL;DR

En Lovable-app er bare så trygg som Supabase-databasen sin, og “RLS er på” beviser svært lite. Test med to brukere i to selskaper, hold roller utenfor brukermetadata, hold den hemmelige nøkkelen utenfor nettleseren, og sjekk tilganger, views, storage, funksjoner og rate limits hver for seg, for hver av dem går utenom policyene dine på sin egen måte. Under er listen jeg kjører, prompten du kan lime inn i Lovable eller Claude, og åtte sjekker jeg la til etter at en klients Lovable-app viste meg hva de første tolv overser.

Jeg er Denys, fullstack- og AI-ingeniør. Denne måneden rettet jeg opp en klients app bygget med Lovable. Alt kjørte på Supabase, som vanlig, og det var så mange sikkerhetsrettinger at det føltes som om appen var laget av dem. Dette er listen jeg nå går gjennom på hvert Lovable- eller Supabase-prosjekt før jeg kaller kundedata beskyttet.

Er Lovable trygt? Sikkerhetsproblemene så langt

Plattformen bygger og hoster appen din, men om appen er trygg avhenger mest av Supabase-databasen bak den, fordi appen leser og skriver i databasen rett fra nettleseren. Den offentlige oversikten viser begge typer problemer:

Når Hva som skjedde Kilde
Mars 2025 CVE-2025-48757: en skanning av 1 645 Lovable-showcase-apper fant 303 endepunkter i 170 av dem som ga data til alle med den offentlige nøkkelen, fordi RLS manglet eller var for løs Matt Palmers redegjørelse, mai 2025
Februar 2026 Én Lovable-hostet app eksponerte 18 697 brukerposter; en tilgangssjekk i funksjonene slapp inn anonyme besøkende og stengte ute innloggede brukere. Lovable sa at deler av koden ikke var generert av Lovable The Register, 27. februar 2026
3. februar – 20. april 2026 En regresjon på plattformen lot andre Lovable-brukere lese chathistorikken og kildekoden til offentlige prosjekter Lovables hendelsespost, 22. april 2026

De to første er hull på app-nivå, den typen sjekkene under fanger opp. Den tredje var Lovables egen, og Lovable rettet den; din del er å holde nøkler ute av chatter og kode (sjekk 3) og bytte ut alle som har ligget der. Lovable kjører en sikkerhetsskanning før publisering, men det er opp til deg å handle på den, og da CVE-en ble publisert, sjekket skanningen at en policy fantes, ikke at den var riktig. Så “er Lovable trygt” kommer ned til om databasen din består sjekkene under.

Hvorfor er “RLS er på” ikke nok i en Lovable-app?

En Lovable-app snakker med databasen direkte fra nettleseren. Det finnes ingen egen backend imellom: siden kaller Supabase Data API med en offentlig nøkkel, og Postgres avgjør hva som kommer tilbake (hvilke skjemaer det API-et serverer, er en egen innstilling, som Supabase exposed schemas tar for seg). Så hver regel som beskytter kundedata, må ligge i databasen, og databasen har flere dører.

Row level security er én av dem. Tilganger (grants) avgjør om en rolle i det hele tatt får røre en tabell, policyer avgjør hvilke rader, og tre ting hopper helt over policyer: den hemmelige nøkkelen, views opprettet uten security_invoker og SECURITY DEFINER-funksjoner. Dette er ikke hypotetisk. Lovables egen sikkerhetsguide ber deg gå gjennom RLS-policyene den setter opp, og CVE-2025-48757 ovenfor er det som skjer når ingen gjør det.

Fire veier en forespørsel når dataene dine. Bare den første sjekkes av RLS-policyene dine. Vanlig forespørsel: Nettleser → Tilgang → RLS-policy → Egne rader; Hemmelig nøkkel i edge function: Nettleser → Edge function → Hemmelig nøkkel → Alle rader; View uten security_invoker: Nettleser → View → Eierens rettigheter → Alle rader; SECURITY DEFINER-funksjon: Nettleser → RPC → Eierens rettigheter → Alle rader.
Figur 1. Bare den første veien sjekkes av policyene dine. De tre andre kjører med rettigheter som hopper over RLS. Kilde: Supabase-dokumentasjon om RLS og sikring av API-et, oktober 2026.

Prompten: lim den inn i Lovable eller Claude

Du kan lime den inn som den er. Sjekk resultatet selv etterpå: AI elsker å si “ferdig” når det ikke stemmer, og en prompt som får en feil til å forsvinne, kan gjøre det ved å gi mer tilgang.

Gå gjennom dette prosjektet for sikkerhet, rett det du finner, og fortell meg hva du endret:

1. Test med to brukere i to selskaper. Bruker A prøver å lese, redigere og slette Bs poster via id, API, søk og eksport. Alt må feile.
2. RLS på hver tabell, policyer sjekker radens eier eller selskap. "Innlogget" er ikke nok.
3. Service role-nøkkelen aldri i frontend. Edge functions som bruker den, sjekker brukeren og filtrerer på selskap selv, nøkkelen hopper over RLS.
4. Roller, abonnement og selskaps-id aldri fra brukermetadata, brukere kan endre dem. Lagre dem i en tabell som bare serveren skriver til.
5. Når en forespørsel sender to id-er (prosjekt + oppgave), sjekk at oppgaven hører til det prosjektet.
6. Trekk tilbake alle tabelltilganger fra anon og authenticated, gi tilbake bare det policyene trenger. RLS stopper ikke TRUNCATE. Hver migrering som oppretter en tabell, inneholder dens GRANT: fra 30. oktober 2026 gir ikke Supabase lenger nye tabeller tilganger automatisk, og appen får feilen 42501.
7. Alle views med security_invoker = true, ellers hopper de over RLS.
8. Storage-buckets: brukere leser og skriver bare egne filer, ingen offentlige buckets for private data.
9. SECURITY DEFINER-funksjoner: NULL må bety avvis, ikke tillat.
10. Rate limits på innlogging, registrering, tilbakestilling og OTP, lagret i databasen, ikke i minnet.
11. Valider alt input på serveren.
12. Kjør Supabase security advisor, rett hver advarsel.

De 12 sjekkene, og hvordan du beviser hver av dem

1. To brukere i to selskaper

Opprett bruker A i selskap 1 og bruker B i selskap 2, hver med noen få poster. Logg inn som A og prøv å lese, redigere og slette Bs poster via id, gjennom API-et, gjennom søk og gjennom hver eksport. Hvert forsøk må returnere ingenting eller en feil. Dette er den ene testen som fanger det meste av det som følger, fordi den tester hva angriperen gjør, ikke hva du skrev.

To brukere, to selskaper. Bruker A i selskap 1 går etter selskap 2 sine poster. Hver vei må returnere ingenting. Via id gjennom API-et: Bruker A → Bs post-id → Les, rediger, slett → 0 rader eller en feil; Gjennom søk: Bruker A → Søk i Bs tekst → Resultater → Ingen B-rader; Gjennom en eksport: Bruker A → Eksport → Fil → Ingen B-rader.
Figur 2. To-selskaper-testen. Bestått betyr at hver vei returnerer ingenting fra selskap 2.

Legg til en tredje bruker som tilhører begge selskapene. En policy på formen “den som kaller er medlem av radens selskap” lar den brukeren lese selskap 2 sine rader mens vedkommende jobber i selskap 1. I en annen Supabase-app returnerte proben min på en slik bruker det andre selskapets dokument, helt til spørringen også filtrerte på selskapet til forespørselen. Det skrev jeg opp i en bruker i to organisasjoner som RLS ikke isolerte.

2. RLS på hver tabell, avgrenset til eieren eller selskapet

auth.uid() is not null betyr “enhver innlogget bruker”, som på en offentlig registreringsside betyr hvem som helst. Hver policy sammenligner radens eier eller selskap med den som kaller. List tabellene som fortsatt har RLS av:

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økkelen aldri i frontend

Service role-nøkkelen og den nyere sb_secret_-nøkkelen omgår alle policyer. Søk i den byggede JavaScript-en etter begge, og roter hver nøkkel som noen gang har vært levert ut. Edge functions som bruker den, må selv sjekke hvem brukeren er og filtrere på selskap, for med den nøkkelen gjør ingenting annet det.

4. Roller, abonnement og selskaps-id aldri fra brukermetadata

raw_user_meta_data er det klienten sendte til signUp, og brukere kan endre det senere. I en annen app leste new-user-triggeren den inviterte rollen derfra, så hvem som helst som registrerte seg, kunne sende med eierrollen og bli Owner. Lagre roller, abonnementer og selskaps-id-er i en tabell som bare serveren skriver til, eller i app_metadata. Hele historien: en rolle hentet fra registreringsmetadata.

5. To id-er i én forespørsel: sjekk at de hører sammen

Når en forespørsel bærer en prosjekt-id og en oppgave-id, er det ikke nok å autorisere prosjektet. En bruker med rettigheter på prosjekt A sender A pluss en seksjons-id fra prosjekt B, og raden havner i B. RLS fanger det ikke, fordi insert stempler angriperens eget prosjekt-id, som er akkurat det policyen sjekker. Sjekk at barnet hører til forelderen før du skriver.

6. Tilganger: trekk tilbake, gi så tilbake bare det som trengs

Tilganger og policyer er to adskilte lag. Å legge til policyer tar ikke en tilgang tilbake, og derfor sier Supabases RLS-guide at du eksplisitt skal trekke tilbake og gi tilbake. I et annet prosjekt hadde supabase/postgres 17.6.1-imaget gitt anon og authenticated TRUNCATE, REFERENCES, TRIGGER og MAINTAIN på hver tabell i public, og ingen policy gjelder for TRUNCATE. Data API-et har ikke noe TRUNCATE-endepunkt, så dette er ikke en sletting med én forespørsel, men ingen klientrolle trenger det, og hver funksjon som kjører SQL som den rollen, 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 endrer denne sjekken. Supabase slutter å gi nye tabeller tilganger automatisk: nye prosjekter siden 30. mai 2026 og alle eksisterende prosjekter fra 30. oktober 2026, også de Lovable har opprettet. Etter det er en tabell som Lovable eller en migrering oppretter uten en eksplisitt GRANT, usynlig for appen: forespørselen feiler med 42501, og funksjonen som bruker tabellen slutter å virke. Så hver migrering som oppretter en tabell, bærer nå sine tilganger, ved siden av enable row level security og policyene.

To ting er som før. Eksisterende tabeller beholder tilgangene de har, så det er fortsatt jobben din å trekke tilbake det du ikke trenger. Og endringen tar bare tilbake select, insert, update og delete: i Lovable-prosjektet kom nye tabeller med de nye standardene fortsatt med TRUNCATE, REFERENCES, TRIGGER og MAINTAIN for anon og authenticated. Trekk først alt tilbake, gi så tilbake, er fortsatt den trygge rekkefølgen.

7. Hvert view med security_invoker = true

Et view kjører med eierens rettigheter, og i Supabase er eieren vanligvis postgres, så et view over en beskyttet tabell deler ut hver rad som policyene holder tilbake. På Postgres 15 og nyere:

alter view public.customer_summary set (security_invoker = true);

8. Storage: bare egne filer, ingen offentlig bucket for private data

En offentlig bucket serverer hvilken som helst fil til alle som har URL-en. Private filer hører hjemme i en privat bucket med policyer på storage.objects som sjekker eieren eller selskapet, vanligvis ut fra den første mappen i stien. Etter at du har flyttet et prosjekt: tell de aktive storage-policyene. I én app feilet hver opplasting etter en flytting, fordi policy-migreringen aldri ble kjørt og tabellen hadde RLS på uten en eneste policy.

9. SECURITY DEFINER-funksjoner: NULL betyr avvis

I PL/pgSQL kaster if not (has_permission or assignee_id = auth.uid()) ingen feil når assignee_id er null: hele betingelsen er null, og if hopper over den. Det samme uttrykket er trygt i en policy, som behandler null som avvis. I én app lot det en rolle med alle tilganger av handle på alle 3683 oppgavene, fordi ingen hadde en ansvarlig. Pakk betingelsen inn i coalesce(…, false). Detaljer: en NULL i PL/pgSQL IF NOT hoppet over sjekken.

10. Rate limits lagret i databasen

Begrens innlogging, registrering, tilbakestilling av passord og OTP, og hold telleren i Postgres. I Lovable-appen lå grensene i en variabel inne i edge function-en. Jeg probet det hostede prosjektet 17. oktober 2026: hver forespørsel kjørte i en ny isolate, så telleren i minnet var alltid 1 og grensen kunne aldri utløses. Lokal supabase functions serve gjenbruker isolate-en, så den samme koden besto alle lokale tester.

Hvor telleren bor, avgjør om den utløses. Rate limit i en hostet Supabase edge function. Teller i funksjonens minne: Forespørsel → Ny isolate → Teller = 1 → Aldri begrenset; Teller i Postgres: Forespørsel → claim_rate_limit → Delt rad → 429 ved grensen.
Figur 3. Proben min av en hostet Supabase edge function, 17. oktober 2026: hver forespørsel startet en ny isolate.

Nøkkelen betyr like mye som lageret. På den hostede plattformen er det siste X-Forwarded-For-leddet Supabases egen proxy, så å bruke det som nøkkel legger alle som kaller i én bøtte, og x-client-ip settes av klienten. I proben min bar cf-connecting-ip den ekte klienten. Løsningen var en SECURITY DEFINER-funksjon claim_rate_limit(bucket, key, max, window) som bare service role kan kjøre, serialisert med en advisory lock, som lagrer en hash av IP-adressen eller bruker-id-en i stedet for råverdien.

11. Valider alt input på serveren

Uansett hva skjemaet sjekker, godtar API-et alt klienten sender. Lengder, formater, enums og eierskap sjekkes i databasen (constraints, triggere) eller i edge function-en, aldri bare i React.

12. Supabase Security Advisor, hver advarsel

Åpne Advisors, deretter Security Advisor, og rett hvert funn. Den flagger tabeller med RLS av, views som hopper over RLS, policyer som leser brukermetadata, åpne using (true)-policyer og funksjoner med en foranderlig search_path. Den ser ikke sjekk 1, 5, 9 eller 10, fordi de handler om logikk, ikke konfigurasjon.

Hva lærte Lovable-appen meg som de 12 overser?

Disse kom ut av klientens app og gjennomgangene av den. Ingen av dem dukker opp i en skanner.

  1. Felt klienten ikke skal sette. Klienten sendte selv status: 'approved', som hoppet over modereringen. En UPDATE-policy der with check er løsere enn using, lar også en bruker flytte en rad til en annen eier. En policy kan ikke beregne en verdi, så en BEFORE INSERT OR UPDATE-trigger utleder status, rolle, pris og selskap på nytt fra serverens egne data og ignorerer det klienten sendte.
  2. Hver skrivevei, ikke bare den primære insert. Etter at den primære insert-en var rettet, satt den samme omgåelsen i vedlegg, kommentarer, gjenoppretting av et mykt slettet element og tellere. List hver skriver til en beskyttet kolonne før du retter den første.
  3. Tillatelseslister, ikke avvisningslister. Forfalskede poster tok fem gjennomgangsrunder, fordi hver rettelse blokkerte én kombinasjon og lot en søsken stå åpen. En trigger som bare tillater de listede kombinasjonene av type og status, lukket hele klassen på én gang.
  4. Funksjoner er også et API. Hver funksjon i public kan kalles via /rpc av enhver rolle med EXECUTE, og RLS gjelder ikke for funksjoner. Gi hver SECURITY DEFINER-funksjon set search_path = '', trekk EXECUTE tilbake fra public, anon og authenticated, og gi det bare til rollen som trenger det.
  5. Å trekke tilbake EXECUTE kan ødelegge offentlige sider. Postgres sjekker EXECUTE på hver funksjon som brukes av hver policy som gjelder for rollen, selv i en gren som aldri ville kjørt. Da anon mistet EXECUTE på is_admin(), feilet hver anonyme lesing med 42501. Policyer bare for administratorer får to authenticated, og offentlige lesinger får sin egen policy uten administratorgrenen.
  6. Registrering med e-postbekreftelse på. Med bekreftelse på returnerer signUp ingen sesjon, så enhver profilskriving rett etterpå kjører som anon og skriver i det stille ingenting. Lovable-prosjekter har ofte auto-confirm på, noe som skjuler feilen. Send feltene i signUp({ options: { data } }), la new-user-triggeren validere og skrive dem, og behandle de metadataene som upålitelige (sjekk 4).
  7. Tester som består av feil grunn. Den lokale stacken og det hostede prosjektet hadde ulike standardrettigheter, så “anon kan ikke sette inn” besto lokalt fordi tilgangen manglet, ikke fordi RLS virket. En negativ test betyr først noe når den positive tilgangen finnes. Sammenlign pg_default_acl lokalt og i produksjon.
  8. Den levende databasen, ikke migreringsfilene. Etter en migrering eller en flytting av prosjektet: sammenlign det som faktisk er live: pg_policies, pg_get_functiondef for hver sperre og tilgangene over. I en annen app kopierte en ny migrering en gammel funksjonskropp og angret i det stille en senere rettelse.

Er en Lovable-app GDPR-kompatibel?

Lovable gjør ikke en app kompatibel eller ukompatibel; det avgjøres av hva appen gjør med personopplysninger, og en sikkerhetsgjennomgang svarer ikke på det. Dette er de seks hullene jeg sjekker i en Lovable Cloud-app, hver med løsningen:

  1. List opp alle databehandlere. Lovable Cloud kjører databasen, auth, lagring og funksjoner på Supabase, Lovable hoster nettstedet og kan sende innloggingseposter, og Lovable AI Gateway sender tekst videre til en modellleverandør. Legg til alle e-post-, AI- eller scrapingtjenester funksjonene dine kaller. Hver tjeneste som mottar personopplysninger, trenger en databehandleravtale, og en utenfor EU/EØS trenger i tillegg et overføringsgrunnlag. Hostingregionen står ikke i koden, så spør etter den.
  2. Slett en bruker som har innhold. Fremmednøkler fra det brukeren har skrevet, kan blokkere slettingen, og da kan en forespørsel om sletting ikke gjennomføres. La de nøklene kaskadere eller settes til null, legg til en slettetrigger som også fjerner brukerens e-postlogger og innloggingstokener, og behold en databasetest som sletter en slik bruker.
  3. Sett en lagringsgrense. E-postlogger, visningssporing, køer med mislykkede e-poster og mykt slettede rader beholder ofte personopplysninger uten sluttdato. En planlagt jobb som sletter mykt slettede rader for godt etter en fast periode, og fjerner filene deres, tetter det meste.
  4. E-postsporing bare med samtykke. Å registrere åpninger og klikk per mottaker krever samtykke etter ePrivacy-reglene: et samtykkeflagg, av som standard, med tidsstempelet satt av serveren, ikke av klienten.
  5. Hold adresser unna funksjonsloggene. Logger fra edge functions blir ikke renset, så en console.log med en mottakeradresse blir liggende der. Logg id-er og antall.
  6. Slå på e-postbekreftelse. Med automatisk bekreftelse kan hvem som helst registrere seg med en adresse de ikke eier, og legge en ekte persons adresse inn i appen. Punkt 6 i listen over dekker registreringssiden.

Slik sjekker du din egen app: søk etter API-nøkler i funksjonene for å liste tjenestene, slett en testbruker med innhold, søk etter @ i funksjonsloggene, og be hver leverandør om databehandleravtalen. Dette er ikke juridisk rådgivning.

Hvordan hindrer du at det kommer tilbake?

Hver sjekk over er en spørring eller en test, så ha dem i repoet og kjør dem før hver utrulling. Supabases pgTAP-tester dekker tilganger, policyer og funksjoner; to-selskaper-testen hører hjemme ved siden av dem. Hvis appen din har roller og en tilgangsskjerm, er en test per policy heller ikke nok: jeg beviser hver tilgangsboks mot en kopi av produksjon, beskrevet i hvordan du tester Supabase RLS, der 30 av 87 bokser viste seg å ikke gjøre noe. Den gratis Supabase sikkerhetssjekk kjører 46 OWASP Top 10:2025-regler mot et prosjekts roller, tilganger, policyer og auth-innstillinger, så bruk den til første gjennomgang; to-selskaper-testen og alt om brukergrensesnittet ditt er fortsatt manuelt.

Hva listen ikke dekker: betalinger og webhooks, hemmeligheter i Git-historikken din, sikkerhetsheadere på verten, sikkerhetskopier. De er viktige, men det er en annen sjekkliste.

Hvis du har arvet en Lovable- eller Supabase-app og er usikker på hvilke av disse den består, er denne gjennomgangen der en prosjektredning starter. Caset om timeregistrering for et revisjonsselskap viser hvordan rollesjekkene ser ut i en produksjonsapp.

Fant du et hull listen overser?

Det vil jeg gjerne høre. Utveksler gjerne erfaringer om å sikre Lovable- og Supabase-apper.

Ta kontakt

© 2026 Denys Zaichenko. Opprinnelig artikkel: https://madsalt.tech/no/blog/lovable-app-security-checklist/