NestJS + Next.js security-hardening: de productieconfiguratie
De concrete security-configuratie die we naar productie uitrollen voor een NestJS-backend en een Next.js-frontend — helmet-standaarden, CSP, rate limits, HSTS en de ene autorisatiefout die we drie teams hebben zien herhalen.
Standaardbeveiliging in NestJS en Next.js is dunner dan de meeste tutorials toegeven. Helmet wordt geleverd met verstandige standaarden die niet genoeg zijn voor B2B-kopers in 2026, de rate limiter heeft Redis-backed opslag nodig om eerlijk te zijn in een multi-replica-deploy, en het autorisatie-guard-patroon dat iedereen kant-en-klaar gebruikt, heeft een subtiele bug die drie van onze klanten naar productie hebben uitgerold. Deze post is de configuratie die we uitrollen.
Backend: NestJS-hardening
Helmet, maar niet de standaarden
De standaard helmet()-call zet een kleine set headers, maar de standaarden zijn niet agressief genoeg voor een B2B-publiek. De configuratie die we uitrollen:
- HSTS met
maxAge: 63072000(2 jaar),includeSubDomains: true,preload: true. Iets korter is een signaal naar de koper dat je het cert gaat roteren en hun pinned client breekt. - frameguard met
action: "deny"— striktDENY, nietSAMEORIGIN. Er is geen UI in onze app die een andere site moet frameten. - referrerPolicy met
policy: "strict-origin-when-cross-origin"— lek de kale origin op cross-origin-navigatie, niets meer. - Content-Security-Policy met een strakke
default-src 'self', dan een expliciete lijst van goedgekeurde image-/script-/connect-bronnen. De CSP is de belangrijkste header voor defense in depth; stem hem af voordat je uitrolt, niet na een security-review.
Throttler, met Redis-backed opslag
NestJS's @nestjs/throttler wordt standaard met in-memory opslag geleverd. Dat is prima in een dev-omgeving en onveilig in productie: elke replica heeft zijn eigen teller, en een aanvaller kan replica's × limiet verzoeken sturen voordat een van hen de throttle doet trippen.
De fix:
- Bedraad
@nest-lab/throttler-storage-rediszodat de teller over replicas wordt gedeeld via Redis. - Zet de storage-URI op dezelfde Redis als je cache. De auth-throttler deelt de bottleneck met de rest van de app, maar dat klopt — het auth-pad is waar de misbruik zich werkelijk concentreert.
Brute-force-bescherming, los van rate limit
Rate limits beantwoorden "hoe vaak kan een enkel IP proberen?" Brute-force-bescherming beantwoordt "wat gebeurt er wanneer een account 5 verkeerde wachtwoorden ziet?" Allebei zijn nodig. De brute-force-laag:
- 5 opeenvolgende fouten op hetzelfde account → 15-minuten-lock, retourneert 429 of 423 Locked.
- Teller reset bij succesvolle login.
- Inactieve accounts worden niet gelockt. (De reden: een aanvaller moet niet in staat zijn een inactief account te locken om te voorkomen dat een legitieme admin het reactiveert.)
- Timing is geëqualiseerd: bcrypt.compare draait tegen een dummy-hash zelfs wanneer het e-mailadres niet bestaat, zodat de reactietijd geen user enumeration lekt.
Autorisatie-guard-fout
De fout die we drie keer in drie verschillende codebases hebben gezien:
// GEVAARLIJK — leest de rol uit een header die de client kan zetten.
const role = request.headers["x-role"];
if (!requiredRoles.includes(role)) throw new ForbiddenException();
De fix:
// CORRECT — leest de rol uit de JWT die de server al heeft geverifieerd.
const role = request.user.role;
if (!requiredRoles.includes(role)) throw new ForbiddenException();
Het x-role-header-patroon omzeilt het hele rollensysteem op het moment dat een ontwikkelaar vergeet de JWT aan een downstream-verzoek te koppelen. Het is een codepad dat compileert, deployt en stilletjes de-escaleert. Centraliseer de rollees op request.user (gevuld door de JWT-strategie) en de bugcategorie verdwijnt.
Input-validatie
class-validator met de globale ValidationPipe is de standaard. De niet-standaard die ertoe doet:
whitelist: trueenforbidNonWhitelisted: true. Als de client een veld stuurt dat niet in de DTO staat, wijs het verzoek af. Verdediging tegen prototype-pollutie en accidentele DTO-drift.- Een FeatureFlag op Joi-/JSON-Schema-validatie voor eindpunten waar bedrijfsregels afhangen van de omgeving, niet van de verzoekvorm.
Frontend: Next.js-hardening
Headers via middleware
De Next.js-middleware draait op elk verzoek vóór de pagina. Gebruik het voor headers die op de HTML-respons moeten worden gezet:
- Content-Security-Policy (echo dezelfde policy die de backend zet, met
frame-ancestors 'none'toegevoegd). - Strict-Transport-Security (HSTS, overeenkomend met de backend, met
max-age=63072000). - X-Content-Type-Options: nosniff.
- Referrer-Policy: strict-origin-when-cross-origin.
- Permissions-Policy: alleen de features die we werkelijk gebruiken (
camera=(self),microphone=(self)voor Tolky; niets anders op het marketingoppervlak).
Beeldoptimalisatie, remote-patterns en de next.config-drift-val
images.remotePatterns is de allowlist voor de next/image-optimizer. De val:
- Een ontwikkelaar voegt een nieuw beelddomein toe, rolt uit en vergeet.
- De next-intl-plugin draait bij runtime en vervangt het
images-blok — laat je custom remotePatterns stil vallen. - Elke afbeelding van dat domein begint 400 te retourneren vanuit de optimizer.
De fix is een runtime-guard die de config opnieuw toepast en een Dockerfile die expliciet de next.config naar de runner-stage kopieert. De guard vangt de drift in productie; de Dockerfile-copy vangt het op buildtijd. Twee lagen om dezelfde reden: laat de next-intl-plugin je beeldoptimalisatiebeleid niet opeten.
Rate-limit de auth-eindpunten vanaf de frontend
Als je kunt, rate-limit op de backend. Als je dat niet kunt (bijv. een externe auth-aanbieder), rate-limit op de frontend-middleware om credential-stuffing-pogingen te vertragen tot een tolerabele rate. Twee lagen.
De gedeelde fout: log-injection
De fout die we in elk NestJS + Next.js-project dat we hebben geaudit, hebben gezien: log-berichten die user-input interpoleren zonder te escapen. Een zoekveld dat logt Zoekquery: ${req.query.q} wordt een log-injectie-vector op het moment dat een aanvaller q=\n[FAKE LOG ENTRY]\n stuurt.
De fix is één regel: escape elke user-input die in een logregel terechtkomt. Bijna elke logging-library heeft een escape- of een structured-logging-patroon dat dit voor je afhandelt. Gebruik het patroon.
Het afsluitende principe
Security is geen feature. Het is de afwezigheid van een klasse bugs. De bovenstaande configuratie elimineert de klassen die we het afgelopen jaar naar productie hebben zien uitrollen. De lijst zal groeien. Pin deze post, review hem per kwartaal en rol een geharde baseline uit.
