AI-sikkerhedVibe codingApplikationssikkerhed
De 7 sikkerhedsfejl vi finder i næsten al AI-genereret kode
Sprogmodeller skriver kode, der virker. Det er præcis problemet. Her er de syv mønstre, vi finder igen og igen i AI-byggede apps.
lejenhacker.dkOpdateret 11 min. læsetid
Et produkt, der før tog seks måneder at bygge, kan i dag være i luften på to uger. Det er en reel og velsignet forskel. Men hastigheden har flyttet sig meget hurtigere end den granskning, koden bliver udsat for, og resultatet er et helt nyt sårbarhedsbillede - ikke fordi AI skriver dårlig kode, men fordi AI skriver kode, der løser præcis det, der stod i prompten, og intet andet.
Vi har gennemgået et større antal applikationer bygget med Cursor, Claude Code, Lovable, Bolt, v0 og Replit Agent. Fejlene går igen med en forudsigelighed, der er næsten kedelig. Her er de syv, vi ser oftest.
1. Adgangskontrollen findes kun i brugerfladen
Det suverænt hyppigste fund. Frontenden skjuler pænt admin-knappen for almindelige brugere, og en middleware sender uautoriserede væk fra /admin. Men det underliggende API-endpoint spørger aldrig, hvem der kalder. En bruger, der åbner netværksfanen i browseren og gentager kaldet med sit eget token, får svaret alligevel.
Årsagen er strukturel: prompten lyder "lav en side hvor admins kan se alle brugere". Modellen bygger siden. At API-laget bag den også skal håndhæve rollen, blev aldrig sagt højt.
// Sådan ser det typisk ud - endpointet stoler på, at kun admins kalder det
export async function GET() {
const users = await db.user.findMany()
return Response.json(users)
}
// Sådan skal det se ud - autorisation håndhæves hvor data hentes
export async function GET() {
const session = await auth()
if (session?.user.role !== 'admin') {
return new Response('Forbidden', { status: 403 })
}
const users = await db.user.findMany()
return Response.json(users)
}2. Row Level Security er aldrig slået til
Supabase, Firebase og PocketBase gør det bevidst nemt at komme i gang: databasen er tilgængelig direkte fra browseren via en offentlig nøgle, og adgangen styres af regler på tabelniveau. Problemet er, at de regler starter åbne, og at AI-værktøjer stort set aldrig skriver dem.
Konsekvensen er, at enhver besøgende kan bruge den offentlige nøgle fra jeres eget frontend-bundle til at læse hele tabeller direkte - uden om jeres backend, jeres validering og jeres logning. Det er ikke en teoretisk risiko. Det er tre linjer i browserkonsollen.
-- Slå RLS til på hver eneste tabel med brugerdata
alter table profiles enable row level security;
-- Og skriv en eksplicit politik. Uden politik = ingen adgang.
create policy "brugere ser kun egen profil"
on profiles for select
using (auth.uid() = user_id);3. Service role-nøglen ligger i klientkoden
Når RLS endelig blokerer noget, er den hurtigste vej udenom at bruge den privilegerede nøgle - den der ignorerer alle regler. Den bliver lagt i en miljøvariabel, og i Next.js får den ofte præfikset NEXT_PUBLIC, fordi det var det, der fik fejlen til at forsvinde.
Alt med præfikset NEXT_PUBLIC bliver bagt ind i den JavaScript, browseren henter. Nøglen er dermed offentlig for alle, der åbner udviklerværktøjerne. Vi finder også jævnligt OpenAI-nøgler, Stripe-hemmeligheder og fulde databaseforbindelser samme sted.
- Søg i jeres byggede frontend-bundle efter "sk-", "service_role", "postgres://" og "SECRET"
- Gennemgå git-historikken - en nøgle, der er fjernet i en senere commit, ligger der stadig
- Antag at enhver lækket nøgle er kompromitteret, og rotér den frem for at fjerne den stille
4. Ingen begrænsning på det, der koster penge
AI-byggede apps kalder ofte selv en sprogmodel, sender e-mails eller genererer billeder. Næsten ingen af dem har rate limiting eller et forbrugsloft pr. bruger. En enkelt person med et script kan brænde et månedsbudget af på en time, og der findes bots, der systematisk leder efter netop den slags endpoints.
Det samme gælder login-endpoints uden begrænsning, hvor adgangskoder kan gættes i tusindvis af forsøg, og e-mail-endpoints, der kan bruges til at sende spam fra jeres domæne - med jeres afsenderomdømme som pris.
5. Betalingsstatus kan sættes af brugeren selv
Vi ser regelmæssigt endpoints som POST /api/user/update, der tager hele request-body’en og skriver den direkte til brugerobjektet. Det betyder, at en bruger kan sende feltet "plan": "pro" eller "role": "admin" med, og systemet skriver det lydigt i databasen.
Den beslægtede fejl er webhooks fra betalingsudbydere, hvor signaturen ikke valideres. Enhver kan så sende en opdigtet "betaling gennemført" til jeres endpoint og få adgang gratis.
// Aldrig: skriv hele request-body'en direkte til databasen
await db.user.update({ where: { id }, data: await req.json() })
// I stedet: tillad kun de felter brugeren faktisk må ændre
const { name, avatarUrl } = schema.parse(await req.json())
await db.user.update({ where: { id }, data: { name, avatarUrl } })6. Prompt injection i appens egne AI-funktioner
Bygger appen selv på en sprogmodel, opstår en ny angrebsflade. Hvis brugerens input sendes videre til modellen sammen med en systemprompt, kan brugeren forsøge at overskrive instruktionerne - og har modellen adgang til værktøjer, der kan læse eller ændre data, bliver det alvorligt.
- Antag at alt hvad modellen returnerer, kan være styret af brugeren
- Giv aldrig en model adgang til et værktøj, brugeren ikke selv måtte kalde direkte
- Behandl modeloutput som utroværdigt input, når det bruges videre i systemet
- Sæt et hårdt loft over antal kald og tokens pr. bruger pr. døgn
7. Afhængigheder ingen har set efter
Sprogmodeller foreslår pakker ud fra, hvad der var almindeligt i træningsdataene - ikke ud fra hvad der er vedligeholdt i dag. Vi finder derfor både forældede versioner med kendte sårbarheder og pakker, der er navngivet plausibelt, men som ikke findes. Det sidste er en reel forsyningskæderisiko, fordi angribere aktivt registrerer de pakkenavne, modeller har for vane at hallucinere.
Sådan tjekker I det selv på en eftermiddag
- Log ind som en almindelig bruger, åbn netværksfanen og gentag et administrativt kald med jeres eget token. Får I data ud, har I fund nummer 1.
- Åbn browserkonsollen på jeres eget site og forsøg at læse en tabel direkte med den offentlige databasenøgle.
- Søg i det byggede frontend-bundle efter "secret", "sk-", "service_role" og "postgres://".
- Kald jeres dyreste endpoint 200 gange i træk og se, om noget stopper jer.
- Send et ekstra felt som "role" eller "plan" med i en profilopdatering og se, om det bliver gemt.
- Kør en afhængighedsgennemgang og kontrollér, at hver pakke faktisk eksisterer og vedligeholdes.
Finder I noget på den liste, er der næsten altid mere. Fejlene optræder sjældent alene, fordi de har samme rod: koden blev skrevet ud fra, hvad den skulle kunne - ikke ud fra, hvad den ikke måtte kunne.
// RELATERET_YDELSE
Vibe Code Audit - sikkerhedsgennemgang af AI-genereret kode
En Vibe Code Audit er en sikkerhedsgennemgang af en applikation, der primært er bygget med AI-kodeværktøjer. Vi gennemgår både kildekoden og den kørende app for de fejlmønstre, sprogmodeller gentager systematisk - manglende adgangskontrol på API-endpoints, hemmeligheder i klientkoden, åbne databaseregler og autentifikation der kun håndhæves i frontenden.
Læs mere om vibe code audit →Korte svar
Er AI-genereret kode usikker?
AI-genereret kode er ikke usikker linje for linje, men den fejler systematisk på det, der ikke stod i prompten - typisk autorisation, rate limiting og inputvalidering. Kombineret med at koden ser velstruktureret ud og derfor sjældnere granskes kritisk, giver det et forudsigeligt sårbarhedsmønster.
Hvordan tester jeg selv min AI-byggede app for sikkerhedshuller?
Start med at gentage et administrativt API-kald med en almindelig brugers token, forsøg at læse databasen direkte fra browserkonsollen, søg efter nøgler i jeres frontend-bundle, og send et ekstra felt som "role" med i en profilopdatering. De fire tests fanger de mest almindelige og alvorlige fejl på under en time.