Next.js Middleware-Sicherheit: Von CVE-2025-29927 zu proxy.ts und Defense-in-Depth

CVE-2025-29927 hat gezeigt: Middleware allein reicht nicht. Erfahre, wie du mit proxy.ts in Next.js 16 und einer mehrschichtigen Defense-in-Depth-Architektur wirklich sichere Anwendungen baust.

Warum die Middleware-Sicherheit in Next.js uns alle betrifft

Mal ehrlich: Middleware war seit Next.js 12 so etwas wie das Schweizer Taschenmesser der Webentwicklung. HTTP-Anfragen abfangen, bevor sie die App erreichen — perfekt für Authentifizierung, Routing und Sicherheitschecks. Klingt erstmal großartig, oder?

Das Problem ist nur: Je mehr wir uns auf Middleware verlassen haben, desto größer wurde die Angriffsfläche. Und dann kam CVE-2025-29927.

Die kritische Sicherheitslücke im März 2025 hat uns schmerzhaft gezeigt, was passiert, wenn Middleware zur einzigen Verteidigungslinie wird. In diesem Artikel schauen wir uns an, wie die Schwachstelle funktioniert, warum Next.js 16 den Wechsel zu proxy.ts eingeführt hat, und wie man mit Defense-in-Depth eine Anwendung baut, die wirklich sicher ist. Mit echten Codebeispielen, versteht sich.

Die Architektur der Next.js Middleware verstehen

Was ist Middleware in Next.js überhaupt?

Middleware in Next.js ist im Grunde eine Funktion, die vor jeder Anfrage läuft. Sie sitzt zwischen dem Client und eurem Anwendungscode und kann Anfragen modifizieren, umleiten oder blockieren. Traditionell lief das Ganze auf der Edge Runtime — einer leichtgewichtigen JavaScript-Umgebung, die möglichst nah am Benutzer ausgeführt wird.

So sieht eine typische Middleware-Datei aus:

// middleware.ts (Next.js 15 und früher)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("session-token");

  // Geschützte Routen prüfen
  if (request.nextUrl.pathname.startsWith("/dashboard")) {
    if (!token) {
      return NextResponse.redirect(new URL("/login", request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*"],
};

Die Matcher-Konfiguration

Der matcher bestimmt, auf welche Routen die Middleware angewendet wird. Das ist wichtiger als man denkt, denn ohne Matcher läuft die Middleware bei jeder einzelnen Route — einschließlich vorgefetchter Routen. Eine sorgfältige Konfiguration spart hier unnötige Ausführungen:

export const config = {
  matcher: [
    // Alle Pfade außer statische Dateien und API-Routen
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

Edge Runtime vs. Node.js Runtime

Hier wird's spannend. Die Edge Runtime bietet minimale Latenz und globale Verteilung, ist aber in ihren APIs eingeschränkt — kein Zugriff auf das Dateisystem, keine nativen Node.js-Module und begrenzte Paketunterstützung. Die Node.js Runtime bietet dagegen vollen API-Zugriff, hat aber höhere Latenz.

Genau diese Einschränkung hat dazu geführt, dass viele Entwickler (mich eingeschlossen, wenn ich ehrlich bin) sensible Logik in die Middleware verlagert haben. Statt sie dort zu belassen, wo sie eigentlich hingehört: in der Datenzugriffsschicht.

CVE-2025-29927: Die kritische Middleware-Sicherheitslücke

Was ist CVE-2025-29927?

Am 21. März 2025 wurde eine Sicherheitslücke veröffentlicht, die mit einem CVSS-Score von 9.1 bewertet wurde. Das ist fast die Höchstwertung. Die Schwachstelle ermöglichte es Angreifern, die gesamte Middleware-Logik zu umgehen — einschließlich Authentifizierung und Autorisierung.

Kurz gesagt: Ein Alptraum.

Technische Analyse des Angriffs

Der Angriff basierte auf dem internen HTTP-Header x-middleware-subrequest. Next.js hat diesen Header intern genutzt, um anzuzeigen, dass eine Anfrage eine interne Unteranfrage der Middleware selbst ist — damit keine Endlosschleifen entstehen.

Das Problem? Externe Anfragen konnten diesen Header genauso setzen. Ein Angreifer musste nur Folgendes tun:

# Exploit-Beispiel (nur zu Bildungszwecken)
curl -H "x-middleware-subrequest: middleware" \
  https://example.com/admin/dashboard

Der Header-Wert musste den Pfad der Middleware-Datei enthalten. In den meisten Anwendungen war das schlicht middleware oder src/middleware. Durch wiederholtes Angeben des Dateinamens (getrennt durch Doppelpunkte) konnte man die interne Rekursionsprüfung austricksen:

# Fortgeschrittener Exploit für verschiedene Next.js-Versionen
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" \
  https://example.com/api/admin/users

Ja, so einfach war das. Erschreckend, oder?

Betroffene Versionen

Die Schwachstelle betraf praktisch alle gängigen Next.js-Versionen:

  • Next.js 12.x: Alle Versionen vor 12.3.5
  • Next.js 13.x: Alle Versionen vor 13.5.9
  • Next.js 14.x: Alle Versionen vor 14.2.25
  • Next.js 15.x: Alle Versionen vor 15.2.3

Auswirkungen der Schwachstelle

Die Konsequenzen waren ziemlich verheerend:

  • Autorisierungs-Bypass: Zugriff auf geschützte Admin-Bereiche und sensible Daten — ganz ohne Anmeldung
  • CSP-Umgehung: Content-Security-Policy-Header, die in der Middleware gesetzt wurden, ließen sich umgehen
  • Cache-Poisoning: Angreifer konnten Caches mit nicht autorisierten Inhalten vergiften
  • CSRF-Schutz-Umgehung: Die Token-Validierung in der Middleware wurde komplett übersprungen

Der Fix

Die Behebung umfasste zwei Maßnahmen:

  1. Header-Stripping: Alle internen HTTP-Header (einschließlich x-middleware-subrequest) werden jetzt aus externen Anfragen entfernt
  2. Kryptographische Validierung: Der Header-Wert wird gegen einen zufällig generierten hexadezimalen String validiert, den nur der Server kennt
// Vereinfachte Darstellung des Fixes
function validateSubrequest(header: string, secret: string): boolean {
  // Nur interne Anfragen mit gültigem Secret werden akzeptiert
  const [name, token] = header.split(":");
  return token === secret;
}

Sofortmaßnahmen bei fehlender Update-Möglichkeit

Falls ein Update nicht sofort möglich war (kennen wir alle — Legacy-Systeme halt), empfahl Vercel diese Gegenmaßnahmen:

# Nginx-Konfiguration zum Blockieren des Headers
location / {
    proxy_set_header x-middleware-subrequest "";
    proxy_pass http://nextjs_backend;
}

# Oder in einer WAF-Regel (z.B. Cloudflare)
# Blockiere alle Anfragen mit dem Header x-middleware-subrequest

Die Migration zu proxy.ts in Next.js 16

Warum proxy.ts?

Next.js 16 hat eine ziemlich fundamentale Änderung eingeführt: middleware.ts wird zu proxy.ts. Und das ist deutlich mehr als nur eine kosmetische Umbenennung.

Der Name "Proxy" macht explizit klar, was die Datei tatsächlich ist: ein Netzwerk-Proxy zwischen externen Anfragen und eurer Anwendung. Das räumt mit einigen hartnäckigen Missverständnissen auf:

  • Es ist kein Express-Middleware-Äquivalent
  • Es ist keine generische Geschäftslogik-Schicht
  • Es ist kein Ersatz für serverseitige Authentifizierung
  • Es ist ein Netzwerk-Proxy — für Routing, Rewrites und Header-Manipulation

Technische Änderungen im Überblick

Hier sind die wichtigsten Unterschiede zwischen middleware.ts und proxy.ts:

Eigenschaft middleware.ts (Next.js 15) proxy.ts (Next.js 16)
Runtime Edge Runtime (Standard) Node.js Runtime (fest)
API-Zugriff Eingeschränkt (Web-APIs) Voller Node.js-Zugriff
Exportname middleware() proxy()
Konfigurierbare Runtime Ja Nein (immer Node.js)
Status Veraltet (deprecated) Empfohlen

Schritt-für-Schritt-Migration

Die gute Nachricht: Die Migration ist dank des offiziellen Codemods relativ unkompliziert:

# Automatische Migration mit dem offiziellen Codemod
npx @next/codemod@latest upgrade

Wer es lieber manuell macht (manchmal hat man einfach mehr Kontrolle), hier die einzelnen Schritte:

Schritt 1: Datei umbenennen

mv middleware.ts proxy.ts
# oder
mv src/middleware.ts src/proxy.ts

Schritt 2: Exportnamen aktualisieren

// Vorher: middleware.ts
export function middleware(request: NextRequest) {
  // ...
}

// Nachher: proxy.ts
export function proxy(request: NextRequest) {
  // ...
}

Schritt 3: Edge-Runtime-Konfiguration entfernen

// Diese Zeile entfernen — proxy.ts läuft immer auf Node.js
// export const runtime = "edge";  // ENTFERNEN

Schritt 4: Node.js-spezifische APIs nutzen (optional, aber empfehlenswert)

// proxy.ts — jetzt mit vollem Node.js-Zugriff
import { proxy } from "next/server";
import { createHash } from "crypto";

export function proxy(request: NextRequest) {
  // Jetzt können native Node.js-Module verwendet werden
  const hash = createHash("sha256")
    .update(request.ip ?? "unknown")
    .digest("hex");

  // Rate-Limiting, Logging etc. mit vollem Node.js-API-Zugang
  return NextResponse.next();
}

Vollständiges proxy.ts-Beispiel

Hier ein praxisnahes Beispiel, wie eine moderne proxy.ts-Datei aussehen kann:

// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// Routen-Definitionen
const PUBLIC_ROUTES = ["/", "/login", "/register", "/api/auth"];
const ADMIN_ROUTES = ["/admin"];

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Sicherheits-Header setzen
  const response = NextResponse.next();
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");

  // 2. Öffentliche Routen durchlassen
  if (PUBLIC_ROUTES.some((route) => pathname.startsWith(route))) {
    return response;
  }

  // 3. Optimistischer Session-Check (nur Cookie lesen, KEINE DB-Abfrage)
  const sessionToken = request.cookies.get("session-token")?.value;
  if (!sessionToken) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 4. Admin-Routen: Rolle aus Cookie prüfen (optimistisch)
  if (ADMIN_ROUTES.some((route) => pathname.startsWith(route))) {
    const role = request.cookies.get("user-role")?.value;
    if (role !== "admin") {
      return NextResponse.redirect(new URL("/unauthorized", request.url));
    }
  }

  return response;
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|public/).*)",
  ],
};

Defense-in-Depth: Mehrschichtige Sicherheit, die wirklich funktioniert

Das Prinzip der Tiefenverteidigung

CVE-2025-29927 hat eine Lektion erteilt, die man nicht vergisst: Middleware darf niemals die einzige Sicherheitsschicht sein. Das Defense-in-Depth-Prinzip fordert mehrere unabhängige Sicherheitsschichten. Wenn eine versagt, fangen die anderen den Angriff ab.

In Next.js empfiehlt sich eine vierschichtige Architektur:

  1. Schicht 1 — Proxy/Middleware: Optimistische Checks, Routing, Header
  2. Schicht 2 — Server Components: Echte Session-Validierung auf dem Server
  3. Schicht 3 — Server Actions: Autorisierung bei jeder einzelnen Mutation
  4. Schicht 4 — Data Access Layer (DAL): Authentifizierung direkt an der Datenquelle

Schauen wir uns jede Schicht im Detail an.

Schicht 1: Der Proxy (Optimistische Prüfung)

Der Proxy führt nur leichtgewichtige Checks durch. Er liest Cookies und prüft, ob sie da sind — aber er validiert keine Sessions gegen die Datenbank. Das ist ein wichtiger Unterschied:

// proxy.ts — Schicht 1
export function proxy(request: NextRequest) {
  const session = request.cookies.get("session");

  // Nur prüfen, OB ein Cookie existiert — nicht ob es gültig ist
  if (!session && isProtectedRoute(request.nextUrl.pathname)) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

Schicht 2: Server Components (Session-Validierung)

Hier passiert die eigentliche Validierung. In Server Components wird die Session tatsächlich gegen die Datenbank geprüft:

// app/dashboard/page.tsx — Schicht 2
import { redirect } from "next/navigation";
import { validateSession } from "@/lib/auth";

export default async function DashboardPage() {
  const session = await validateSession();

  if (!session) {
    redirect("/login");
  }

  if (session.user.role !== "admin") {
    redirect("/unauthorized");
  }

  return (
    

Dashboard

Willkommen, {session.user.name}

); }

Schicht 3: Server Actions (Mutations-Autorisierung)

Jede Server Action muss eigenständig die Berechtigung prüfen. Auch wenn die aufrufende Seite schon geschützt ist! Klingt redundant, ist aber genau der Punkt:

// app/actions/user.ts — Schicht 3
"use server";

import { validateSession } from "@/lib/auth";
import { revalidatePath } from "next/cache";
import { z } from "zod";

const updateProfileSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
});

export async function updateProfile(formData: FormData) {
  // 1. Authentifizierung prüfen
  const session = await validateSession();
  if (!session) {
    throw new Error("Nicht authentifiziert");
  }

  // 2. Eingaben validieren
  const parsed = updateProfileSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
  });

  if (!parsed.success) {
    return { error: "Ungültige Eingaben" };
  }

  // 3. Autorisierung prüfen (Benutzer darf nur eigenes Profil ändern)
  const userId = session.user.id;

  // 4. Daten aktualisieren über den Data Access Layer
  await updateUserProfile(userId, parsed.data);

  revalidatePath("/dashboard/profile");
  return { success: true };
}

Schicht 4: Data Access Layer (Die letzte Bastion)

Die letzte und meiner Meinung nach wichtigste Schicht: Autorisierungsprüfungen direkt in der Datenzugriffsschicht. Selbst wenn alle vorherigen Schichten umgangen werden — der DAL schützt die Daten:

// lib/dal/user.ts — Schicht 4
import { db } from "@/lib/db";
import { validateSession } from "@/lib/auth";
import { cache } from "react";

// Wiederverwendbare, gecachte Authentifizierungsprüfung
export const getCurrentUser = cache(async () => {
  const session = await validateSession();
  if (!session) {
    throw new Error("Nicht authentifiziert");
  }
  return session.user;
});

// Geschützter Datenzugriff
export async function getUserOrders(targetUserId: string) {
  const currentUser = await getCurrentUser();

  // Autorisierung: Nur eigene Bestellungen oder Admin
  if (currentUser.id !== targetUserId && currentUser.role !== "admin") {
    throw new Error("Nicht autorisiert");
  }

  return db.query.orders.findMany({
    where: (orders, { eq }) => eq(orders.userId, targetUserId),
    orderBy: (orders, { desc }) => [desc(orders.createdAt)],
  });
}

// Admin-geschützter Datenzugriff
export async function getAllUsers() {
  const currentUser = await getCurrentUser();

  if (currentUser.role !== "admin") {
    throw new Error("Admin-Berechtigung erforderlich");
  }

  return db.query.users.findMany();
}

Wie alles zusammenspielt

Das Schöne an diesem Ansatz: Selbst wenn eine Schicht kompromittiert wird, halten die anderen stand.

// Anfrage: GET /admin/users

// Schicht 1 (proxy.ts): Cookie vorhanden? → Ja → Weiter
// Schicht 2 (page.tsx): Session gültig? Rolle = Admin? → Ja → Rendern
// Schicht 3 (action.ts): Bei jeder Mutation erneut prüfen
// Schicht 4 (dal.ts): Vor jedem Datenzugriff validieren

// Selbst wenn Schicht 1 umgangen wird (wie bei CVE-2025-29927):
// → Schicht 2 blockiert ungültige Sessions
// → Schicht 3 verhindert unautorisierte Mutationen
// → Schicht 4 schützt die Daten direkt an der Quelle

Erweiterte Sicherheitsmuster

Rate Limiting im Proxy

Mit der Node.js-Runtime in proxy.ts kann man jetzt deutlich anspruchsvollere Sicherheitsmechanismen umsetzen. Hier ein einfaches In-Memory Rate Limiting (für Produktionsumgebungen sollte man natürlich Redis oder ähnliches nutzen):

// proxy.ts mit einfachem In-Memory Rate Limiting
const rateLimitMap = new Map();

function checkRateLimit(ip: string, limit = 100, windowMs = 60000): boolean {
  const now = Date.now();
  const entry = rateLimitMap.get(ip);

  if (!entry || now > entry.resetTime) {
    rateLimitMap.set(ip, { count: 1, resetTime: now + windowMs });
    return true;
  }

  if (entry.count >= limit) {
    return false;
  }

  entry.count++;
  return true;
}

export function proxy(request: NextRequest) {
  const ip = request.ip ?? request.headers.get("x-forwarded-for") ?? "unknown";

  // API-Routen rate-limitieren
  if (request.nextUrl.pathname.startsWith("/api/")) {
    if (!checkRateLimit(ip)) {
      return NextResponse.json(
        { error: "Zu viele Anfragen" },
        { status: 429 }
      );
    }
  }

  return NextResponse.next();
}

Geolocation-basierte Weiterleitung

// proxy.ts — Sprach-Weiterleitung basierend auf Geolocation
export function proxy(request: NextRequest) {
  const country = request.geo?.country;
  const pathname = request.nextUrl.pathname;

  // Nur auf der Startseite weiterleiten
  if (pathname === "/") {
    const locale = getLocaleForCountry(country);
    if (locale && locale !== "en") {
      return NextResponse.redirect(
        new URL(`/${locale}`, request.url)
      );
    }
  }

  return NextResponse.next();
}

function getLocaleForCountry(country?: string): string {
  const countryLocaleMap: Record = {
    DE: "de",
    AT: "de",
    CH: "de",
    FR: "fr",
    ES: "es",
  };
  return countryLocaleMap[country ?? ""] ?? "en";
}

Content-Security-Policy (CSP) mit Nonce

Etwas, das man in jeder ernsthaften Anwendung implementieren sollte:

// proxy.ts — CSP mit dynamischem Nonce
import { randomBytes } from "crypto";

export function proxy(request: NextRequest) {
  const nonce = randomBytes(16).toString("base64");
  const response = NextResponse.next();

  // CSP-Header mit Nonce setzen
  const cspHeader = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self'",
    "frame-ancestors 'none'",
  ].join("; ");

  response.headers.set("Content-Security-Policy", cspHeader);
  response.headers.set("x-nonce", nonce);

  return response;
}

Authentifizierung mit Auth.js v5

Integration mit proxy.ts

Auth.js (ehemals NextAuth.js) v5 ist bereits auf die neue Architektur vorbereitet. Die Integration ist erfreulich unkompliziert:

// auth.ts — Auth.js v5 Konfiguration
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/lib/db";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    GitHub,
    Credentials({
      credentials: {
        email: { label: "E-Mail" },
        password: { label: "Passwort", type: "password" },
      },
      authorize: async (credentials) => {
        // Benutzer validieren
        const user = await verifyCredentials(credentials);
        return user;
      },
    }),
  ],
  callbacks: {
    authorized: async ({ auth, request }) => {
      // Diese Callback wird im Proxy verwendet
      return !!auth;
    },
  },
});
// proxy.ts — Auth.js v5 Integration
import { auth } from "@/auth";

export const proxy = auth;

export const config = {
  matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
};

Rollenbasierte Zugriffskontrolle (RBAC)

Für die meisten Anwendungen braucht man früher oder später ein Rollenmodell. Hier eine pragmatische Umsetzung:

// lib/auth.ts — RBAC-Hilfsfunktionen
import { auth } from "@/auth";
import { cache } from "react";

export type Role = "user" | "editor" | "admin";

interface Permission {
  resource: string;
  action: "read" | "write" | "delete";
}

const rolePermissions: Record = {
  user: [
    { resource: "profile", action: "read" },
    { resource: "profile", action: "write" },
    { resource: "orders", action: "read" },
  ],
  editor: [
    { resource: "profile", action: "read" },
    { resource: "profile", action: "write" },
    { resource: "articles", action: "read" },
    { resource: "articles", action: "write" },
  ],
  admin: [
    { resource: "*", action: "read" },
    { resource: "*", action: "write" },
    { resource: "*", action: "delete" },
  ],
};

export const getSession = cache(async () => {
  const session = await auth();
  return session;
});

export function hasPermission(
  role: Role,
  resource: string,
  action: Permission["action"]
): boolean {
  const permissions = rolePermissions[role];
  return permissions.some(
    (p) =>
      (p.resource === resource || p.resource === "*") &&
      p.action === action
  );
}

Testen der Sicherheitsschichten

Integrationstests für den Proxy

Automatisierte Tests sind hier nicht optional — sie sind Pflicht. Wenn man sich auf mehrere Sicherheitsschichten verlässt, muss man auch sicherstellen, dass jede davon funktioniert:

// __tests__/proxy.test.ts
import { describe, it, expect } from "vitest";
import { NextRequest } from "next/server";
import { proxy } from "../proxy";

describe("Proxy-Sicherheitstests", () => {
  it("leitet unauthentifizierte Benutzer auf /login um", async () => {
    const request = new NextRequest(
      new URL("http://localhost:3000/dashboard")
    );

    const response = await proxy(request);

    expect(response.status).toBe(307);
    expect(response.headers.get("location")).toContain("/login");
  });

  it("lässt authentifizierte Benutzer durch", async () => {
    const request = new NextRequest(
      new URL("http://localhost:3000/dashboard"),
      {
        headers: {
          cookie: "session-token=valid-token",
        },
      }
    );

    const response = await proxy(request);

    expect(response.status).toBe(200);
  });

  it("setzt Sicherheits-Header", async () => {
    const request = new NextRequest(
      new URL("http://localhost:3000/")
    );

    const response = await proxy(request);

    expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
    expect(response.headers.get("X-Frame-Options")).toBe("DENY");
  });

  it("blockiert Zugriff auf Admin-Routen für Nicht-Admins", async () => {
    const request = new NextRequest(
      new URL("http://localhost:3000/admin"),
      {
        headers: {
          cookie: "session-token=valid-token; user-role=user",
        },
      }
    );

    const response = await proxy(request);

    expect(response.status).toBe(307);
    expect(response.headers.get("location")).toContain("/unauthorized");
  });
});

Sicherheitsaudits mit automatisierten Scans

Ergänzend zu den Tests sollte man regelmäßig automatisierte Scans durchführen:

# Abhängigkeiten auf bekannte Schwachstellen prüfen
npm audit

# Next.js-spezifische Sicherheitsprüfung
npx next info

# Überprüfung der Header-Sicherheit
npx is-website-vulnerable https://ihre-domain.de

Checkliste für die Produktionsbereitstellung

Bevor die Anwendung live geht, sollte man diese Punkte durchgehen. Am besten druckt man sich das aus und hakt es physisch ab (ja, das meine ich ernst):

  • Next.js-Version: Mindestens 15.2.3 oder höher, idealerweise 16.x
  • Proxy-Migration: Von middleware.ts zu proxy.ts migriert
  • Defense-in-Depth: Mindestens drei der vier Sicherheitsschichten implementiert
  • Keine Geschäftslogik im Proxy: proxy.ts auf Routing, Header und optimistische Checks beschränkt
  • Session-Validierung: Sessions in Server Components und Server Actions gegen die Datenbank validiert
  • Data Access Layer: Autorisierungsprüfungen direkt in der Datenzugriffsschicht
  • Eingabevalidierung: Alle Eingaben mit Zod oder ähnlichen Bibliotheken validiert
  • CSP-Header: Content-Security-Policy mit dynamischen Nonces gesetzt
  • Sicherheits-Header: X-Content-Type-Options, X-Frame-Options, Referrer-Policy konfiguriert
  • Rate Limiting: API-Endpunkte vor Missbrauch geschützt
  • Automatisierte Tests: Alle Sicherheitsschichten mit Integrationstests abgedeckt
  • Dependency Audits: Regelmäßig npm audit ausgeführt

Fazit

Die Sicherheitslandschaft von Next.js hat sich seit CVE-2025-29927 grundlegend verändert. Die Migration von middleware.ts zu proxy.ts ist nicht einfach nur ein technisches Update — es ist ein Paradigmenwechsel im Sicherheitsdenken.

Was ich aus der ganzen Geschichte mitgenommen habe:

  • Middleware ist kein Sicherheitsgateway: Nutzt proxy.ts für Routing und Header, nicht für Geschäftslogik
  • Defense-in-Depth ist nicht verhandelbar: Mindestens drei Schichten — Proxy, Server Components, Server Actions und Data Access Layer
  • Validierung gehört an die Datenquelle: Der DAL ist die letzte und wichtigste Verteidigungslinie
  • Tests sind Pflicht: Sicherheit muss automatisiert getestet werden, nicht nur manuell

Mit Next.js 16 und dem proxy.ts-Muster haben wir als Entwickler jetzt die Werkzeuge, um wirklich sichere Anwendungen zu bauen. Die Zukunft bringt mit Passkeys und WebAuthn weitere Verbesserungen, aber das Fundament einer mehrschichtigen Sicherheitsarchitektur bleibt zeitlos relevant.

Über den Autor Editorial Team

Our team of expert writers and editors.