NestJS-+-Next.js-Security-Hardening: Die Produktionskonfiguration
Die konkrete Security-Konfiguration, die wir für ein NestJS-Backend und ein Next.js-Frontend in die Produktion ausliefern – Helmet-Defaults, CSP, Rate Limits, HSTS und der eine Autorisierungsfehler, den wir drei Teams wiederholt sehen.
Die Default-Security in NestJS und Next.js ist dünner, als die meisten Tutorials zugeben. Helmet liefert mit vernünftigen Defaults, die für B2B-Käuferinnen 2026 nicht ausreichen, der Rate-Limiter braucht Redis-gestützten Speicher, um in einem Multi-Replica-Deploy ehrlich zu sein, und das Autorisierungs-Guard-Muster, das jeder von der Stange nutzt, hat einen subtilen Bug, den drei unserer Kundinnen in die Produktion ausgeliefert haben. Dieser Beitrag ist die Konfiguration, die wir ausliefern.
Backend: NestJS-Hardening
Helmet, aber nicht die Defaults
Der Default-helmet()-Aufruf setzt einen kleinen Satz Header, aber die Defaults sind für ein B2B-Publikum nicht aggressiv genug. Die Konfiguration, die wir ausliefern:
- HSTS mit
maxAge: 63072000(2 Jahre),includeSubDomains: true,preload: true. Alles Kürzere ist für die Käuferin ein Signal, dass Sie das Zertifikat rotieren und ihren gepinnten Client brechen werden. - frameguard mit
action: „deny“– striktDENY, nichtSAMEORIGIN. Es gibt keine UI in unserer App, die eine andere Site einbetten muss. - referrerPolicy mit
policy: „strict-origin-when-cross-origin“– die bloße Origin bei Cross-Origin-Navigation leaken, nichts mehr. - Content-Security-Policy mit einem strengen default-src 'self', dann einer expliziten Liste freigegebener Image-/Script-/Connect-Quellen. Die CSP ist der wichtigste Header für Defense-in-Depth; stimmen Sie sie ab, bevor Sie ausliefern, nicht nach einem Security-Review.
Throttler, mit Redis-gestütztem Speicher
Das @nestjs/throttler von NestJS liefert standardmäßig In-Memory-Speicher aus. Das ist in einer Dev-Umgebung in Ordnung und in der Produktion unsicher: Jedes Replica hat seinen eigenen Zähler, und ein Angreifer kann Replicas × Limit Anfragen senden, bevor eines von ihnen den Throttler auslöst.
Der Fix:
@nest-lab/throttler-storage-redisverkabeln, sodass der Zähler über Redis über alle Replicas hinweg geteilt wird.- Die Storage-URI auf dasselbe Redis wie den Cache setzen. Der Auth-Throttler teilt sich den Flaschenhals mit dem Rest der App, aber das ist korrekt – der Auth-Pfad ist, wo der Missbrauch tatsächlich konzentriert ist.
Brute-Force-Schutz, separat vom Rate-Limit
Rate-Limits beantworten „wie oft darf eine einzelne IP versuchen?“ Brute-Force-Schutz beantwortet „was passiert, wenn ein Konto 5 falsche Passwörter sieht?“ Beides wird gebraucht. Die Brute-Force-Schicht:
- 5 aufeinanderfolgende Fehlschläge auf demselben Konto → 15-Minuten-Sperre, gibt 429 oder 423 Locked zurück.
- Der Zähler resetet bei erfolgreichem Login.
- Inaktive Konten werden nicht gesperrt. (Der Grund: Ein Angreifer sollte ein inaktives Konto nicht sperren können, um zu verhindern, dass eine legitime Administratorin es reaktiviert.)
- Das Timing ist ausgeglichen: bcrypt.compare läuft gegen einen Dummy-Hash, selbst wenn die E-Mail nicht existiert, sodass die Antwortzeit die Nutzeraufzählung nicht leckt.
Der Autorisierungs-Guard-Fehler
Der Fehler, den wir dreimal in drei verschiedenen Codebases gesehen haben:
// GEFÄHRLICH – liest die Rolle aus einem Header, den der Client setzen kann.
const role = request.headers[„x-role“];
if (!requiredRoles.includes(role)) throw new ForbiddenException();
Der Fix:
// KORREKT – liest die Rolle aus dem JWT, das der Server bereits verifiziert hat.
const role = request.user.role;
if (!requiredRoles.includes(role)) throw new ForbiddenException();
Das x-role-Header-Muster umgeht das gesamte Rollensystem, sobald eine Entwicklerin vergisst, das JWT an einen Downstream-Request anzuhängen. Es ist ein Code-Pfad, der kompiliert, deployt und lautlos eskaliert. Zentralisieren Sie das Rollen-Lesen auf request.user (gefüllt durch die JWT-Strategie) und die Fehlerkategorie verschwindet.
Input-Validierung
class-validator mit der globalen ValidationPipe ist der Default. Das Nicht-Default, das zählt:
whitelist: trueundforbidNonWhitelisted: true. Sendet der Client ein Feld, das nicht im DTO ist, die Anfrage ablehnen. Defense gegen Prototype-Pollution und versehentlichen DTO-Drift.- Ein FeatureFlag auf Joi-/JSON-Schema-Validierung für Endpunkte, bei denen Geschäftsregeln von der Umgebung abhängen, nicht von der Request-Form.
Frontend: Next.js-Hardening
Header über Middleware
Die Next.js-Middleware läuft auf jeder Anfrage, bevor die Seite das tut. Nutzen Sie sie für Header, die auf der HTML-Antwort gesetzt werden müssen:
- Content-Security-Policy (dieselbe Policy wie das Backend echoen, mit ergänztem
frame-ancestors ‚none‘). - Strict-Transport-Security (HSTS, passend zum Backend, mit
max-age=63072000). - X-Content-Type-Options: nosniff.
- Referrer-Policy: strict-origin-when-cross-origin.
- Permissions-Policy: nur die Features, die wir tatsächlich nutzen (
camera=(self),microphone=(self)für Tolky; nichts sonst auf der Marketing-Fläche).
Bildoptimierung, Remote-Patterns und die next.config-Drift-Falle
images.remotePatterns ist die Allowlist für den next/image-Optimizer. Die Falle:
- Eine Entwicklerin fügt eine neue Bild-Domain hinzu, liefert aus und vergisst.
- Das next-intl-Plugin läuft zur Laufzeit und ersetzt den
images-Block – und droppt lautlos die eigenen Remote-Patterns. - Jedes Bild von dieser Domain gibt beim Optimizer 400 zurück.
Der Fix ist ein Runtime-Guard, der die Config erneut anwendet, und ein Dockerfile, das explizit COPY die next.config in die Runner-Stage ausführt. Der Guard fängt den Drift in der Produktion; das Dockerfile-Copy fängt ihn zur Build-Zeit. Zwei Schichten aus demselben Grund: lassen Sie das next-intl-Plugin nicht Ihre Bildoptimierungs-Policy fressen.
Die Auth-Endpunkte vom Frontend her rate-limiten
Wenn möglich, rate-limiten Sie im Backend. Wenn nicht (z. B. ein Drittanbieter-Auth-Provider), rate-limiten Sie in der Frontend-Middleware, um Credential-Stuffing-Versuche auf eine tolerable Rate zu verlangsamen. Zwei Schichten.
Der geteilte Fehler: Log-Injection
Der Fehler, den wir in jedem NestJS-+-Next.js-Projekt gesehen haben, das wir auditiert haben: Log-Meldungen, die Nutzer-Input interpolieren, ohne zu escapen. Ein Suchfeld, das Search query: ${req.query.q} loggt, wird zum Log-Injection-Vektor, sobald ein Angreifer q=\n[FAKE LOG ENTRY]\n sendet.
Der Fix ist eine Zeile: jeden Nutzer-Input escapen, der in einer Log-Zeile landet. Fast jede Logging-Bibliothek hat ein escape oder ein strukturiertes Logging-Muster, das das für Sie erledigt. Nutzen Sie das Muster.
Das Schlussprinzip
Security ist kein Feature. Sie ist die Abwesenheit einer Klasse von Bugs. Die Konfiguration oben eliminiert die Klassen, die wir im vergangenen Jahr in die Produktion ausgeliefert gesehen haben. Die Liste wird wachsen. Pinnen Sie diesen Beitrag, überprüfen Sie ihn vierteljährlich und liefern Sie eine gehärtete Baseline aus.
