Een Next.js-app lanceren met Cursor, v0 of Bolt gaat snel, maar die snelheid verbergt fouten die je pas ziet als iemand ze voor je vindt. Deze checklist loopt door wat je moet nakijken voordat je op deploy drukt, met het waarom van elk punt, hoe je het controleert en de code om het op te lossen. De versie van twee minuten staat aan het eind.
Hoe Next.js de grens tussen server en client trekt
Bijna elke beveiligingsfout in Next.js is dezelfde fout: iets wat op de server had moeten blijven, is naar de browser overgestoken. Server Components draaien op de server en sturen hun code nooit naar de client; zodra een bestand 'use client' draagt, belandt alles wat het importeert in de publieke bundle. Route Handlers (app/api/*/route.ts) en Server Actions zijn publieke URL's, ook al zie je ze nooit. Middleware draait vóór alles, maar is geen muur. Is die grens helder, dan is de rest van de lijst routine.
1. Omgevingsvariabelen: pas op met NEXT_PUBLIC
Waarom het ertoe doet. Alles wat met NEXT_PUBLIC_ begint, wordt tijdens de build in de browser-JavaScript gebakken. Iedereen kan het lezen met de ontwikkelaarstools. Zet daar nooit een privé-API-key, een servicetoken of je databasesecret.
Zo controleer je het. Open je .env en je Vercel-dashboard en lees elke variabele met publiek voorvoegsel met de vraag: zou er iets gebeuren als ik hem op Twitter zet? Is het antwoord ja, haal dan het voorvoegsel weg en verplaats hem naar de server.
Zo los je het op. Variabelen zonder voorvoegsel bestaan alleen op de server: lees ze vanuit Server Components, Route Handlers of Server Actions. Om te garanderen dat een module met secrets nooit vanuit een clientcomponent kan worden geïmporteerd, zet je import 'server-only' op de eerste regel: importeert iemand hem van de verkeerde kant, dan mislukt de build in plaats van dat de key lekt. Meer detail in de secrets die je app in de browser lekt.
2. Route Handlers en Server Actions: altijd valideren en autoriseren
Waarom het ertoe doet. Een API-route is niet privé omdat hij in jouw map staat. Het is een publieke URL die iedereen met curl kan aanroepen. En een Server Action, ook al wordt hij vanuit een knop aangeroepen, evengoed: Next.js stelt hem beschikbaar als een POST-endpoint met een identifier. Dat een knop voor een gebruiker verborgen is, weerhoudt niemand ervan het endpoint rechtstreeks aan te spreken.
Zo los je het op. Controleer de sessie en de rechten binnen elke handler en elke action, niet alleen in de interface. Valideer de request body met een schema (zod of vergelijkbaar) voordat je de database aanraakt. Accepteer de gebruikers-id nooit vanuit de client: haal hem uit de sessie. En zet een rate limit op alles wat geld kost of e-mail verstuurt.
// app/actions/update-profile.ts
'use server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
const Schema = z.object({ name: z.string().min(1).max(80) })
export async function updateProfile(input: unknown) {
const session = await getSession()
if (!session) throw new Error('Unauthorized')
const data = Schema.parse(input)
// gebruik session.user.id, nooit een id uit de client
await db.user.update({ where: { id: session.user.id }, data })
}
3. Middleware is niet je enige authenticatie
Waarom het ertoe doet. Het is verleidelijk om heel /dashboard vanuit middleware.ts te beschermen en er niet meer naar om te kijken. In maart 2025 werd CVE-2025-29927 gepubliceerd: een interne header (x-middleware-subrequest) maakte het mogelijk de middleware in de getroffen versies volledig over te slaan, en daarmee elke sessiecheck die alleen daar leefde. Het is opgelost in Next.js 15.2.3 en 14.2.25 (met patches voor 13 en 12), maar de les blijft.
Zo los je het op. Update Next.js naar een gepatchte versie en gebruik middleware als eerste laag (wie geen sessie heeft omleiden), nooit als de enige. De controle die de data echt beschermt, hoort in de pagina, de handler of de action die ze serveert.
4. Security headers
Waarom het ertoe doet. Standaard stuurt Next.js geen defensieve headers. Zonder staat je app open voor clickjacking, voor een browser die als script uitvoert wat dat niet is, en voor downgrade naar HTTP.
Zo los je het op. In next.config.js:
// next.config.js
const securityHeaders = [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
]
module.exports = {
poweredByHeader: false,
productionBrowserSourceMaps: false,
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
},
}
De Content-Security-Policy verdient aparte aandacht: Next.js injecteert inline scripts om de pagina te hydrateren, dus een strikte CSP heeft een nonce nodig die in de middleware wordt gegenereerd en aan de componenten wordt doorgegeven. Begin in de modus Report-Only. De details van elke header staan in HTTP security headers uitgelegd.
5. Secrets die naar Client Components oversteken
Waarom het ertoe doet. Een privékey belandt makkelijk per ongeluk in een clientcomponent, vooral als de AI bestanden herschrijft en imports verplaatst. Verschijnt hij in de geserveerde JavaScript, dan is hij al gecompromitteerd.
Zo controleer je het. Zoek na next build in .next/static naar strings als sk_live, service_role, sk-ant of webhook-URL's. Komen ze voor, roteer ze dan en verplaats ze naar de server met import 'server-only' als vangnet.
6. Externe afbeeldingen en aanvragen naar URL's van de gebruiker
Waarom het ertoe doet. next/image optimaliseert afbeeldingen via je server. Sta je in images.remotePatterns elke host toe, dan kan iedereen je server als afbeeldingsproxy gebruiken en je quotum opmaken. Hetzelfde geldt voor een API-route die de URL downloadt die de gebruiker meegeeft: die kan naar interne diensten van je infrastructuur wijzen (SSRF).
Zo los je het op. Beperk remotePatterns tot de domeinen die je echt gebruikt. Moet een route externe URL's ophalen, valideer dan het protocol (alleen https), weiger privé-IP's en interne hostnamen, en stel een time-out in.
7. Open redirects
Waarom het ertoe doet. Het klassieke redirect(searchParams.next) na het inloggen laat iedereen links op jouw domein bouwen die op een phishingsite eindigen: jouwdomein.nl/login?next=https://kwaad.example.
Zo los je het op. Accepteer alleen relatieve paden die met / beginnen (en niet met //), of vergelijk met een lijst van toegestane bestemmingen.
8. Bestanden die niet bereikbaar zouden moeten zijn
Waarom het ertoe doet. Alles in public/ wordt as-is geserveerd: een per ongeluk daarheen gekopieerde .env, een databasedump of een service-account.json zijn één URL verwijderd. En source maps in productie laten iedereen je clientcode met de oorspronkelijke namen lezen.
Zo controleer je het. Probeer jouwdomein.nl/.env, /.git/HEAD en open een JS-bestand uit de bundle om te zien of het eindigt op een regel sourceMappingURL die naar een bereikbare .map wijst. Bekijk je map public/ door de ogen van een vreemde.
9. Sessiecookies en auth-bibliotheken
Waarom het ertoe doet. Auth.js (NextAuth), Clerk of Supabase met zijn serverpakket stellen cookies standaard goed in. Wat misgaat is wat jij toevoegt: een eigen cookie zonder HttpOnly, een zwak of uit een tutorial gekopieerd AUTH_SECRET, of de sessie in localStorage.
Zo los je het op. Genereer het secret met openssl rand -base64 32, bewaar het zonder publiek voorvoegsel, en gebruik voor elke eigen cookie Secure, HttpOnly en SameSite=Lax.
10. Dependencies en de Next.js-versie
Waarom het ertoe doet. Je app sleept honderden packages mee, en Next.js zelf brengt meerdere keren per jaar beveiligingsfixes uit. De middleware-zaak van 2025 trof iedereen die niet updatete.
Zo los je het op. npm audit vóór elke deploy, Dependabot of Renovate op de repository, en een eenvoudige regel: Next.js updaten in dezelfde maand als er een beveiligingspatch uitkomt.
Deployen op Vercel: drie details
- Omgevingen. Variabelen hebben een scope (Production, Preview, Development). Geef preview-deployments geen productie-credentials: een preview is een publieke URL met je nieuwste code.
- Preview-bescherming. Zet Deployment Protection aan zodat preview-URL's een Vercel-login vereisen; anders ziet iedereen met de link de branch in uitvoering.
- Headers. Die uit
next.config.jsgelden op Vercel zonder meer; gebruik je daarnaastvercel.json, dupliceer ze dan niet.
Je checklist van 2 minuten
- Geen enkele
NEXT_PUBLIC_-variabele bevat een secret; modules met keys dragenimport 'server-only'. - Elke Route Handler en Server Action controleert de sessie en valideert de invoer; de gebruikers-id komt uit de sessie.
- Next.js op een gepatchte versie; middleware is niet de enige auth-laag.
- Security headers in
next.config.js;poweredByHeader: false; CSP minstens in rapportmodus. - Bundle zonder
sk_live,service_roleof webhooks; source maps uit. remotePatternsbeperkt; niets haalt gebruikers-URL's op zonder validatie.- Redirects alleen naar relatieve paden.
public/zonder gevoelige bestanden;.enven.gitonbereikbaar.- Cookies met Secure, HttpOnly en SameSite; willekeurig
AUTH_SECRET. npm auditschoon; previews beschermd en zonder productiesecrets.
Controleer dit alles in 30 seconden
Deze lijst bij elke deploy met de hand nalopen is saai en wordt vergeten. Scanaris voert read-only controles uit op je gepubliceerde site: headers, secrets in de JavaScript, bereikbare gevoelige bestanden, source maps, cookies en CORS. Elke bevinding komt met een prompt die je zo in Cursor of v0 plakt met de exacte fix voor Next.js. Laat je monitoring aan, dan waarschuwt het je als een vibe coding-sessie weer iets openzet. Om dieper in je stack te duiken is er de beveiligingspagina voor Next.js en de algemene lanceerchecklist.
Veelgestelde vragen
Zijn Server Actions standaard veilig?
Ze zijn handiger dan een API-route, niet veiliger. Next.js stelt ze beschikbaar als publieke endpoints; authenticatie en validatie voeg jij in elk ervan toe.
Beschermt middleware mijn routes?
Het is goed voor omleidingen en goedkope checks, maar het mag niet de enige barrière zijn. Sinds de zaak van 2025 is de regel: controleer de sessie ook waar de data wordt geserveerd.
Heb ik een CSP nodig met Next.js?
Ja, maar met een nonce, en beginnend in rapportmodus. Zonder nonce blokkeert een strikte CSP de inline scripts van het framework zelf en blijft de pagina wit.