Design wechseln

Next.js API-Authentifizierung und Sicherheit: Praxisleitfaden von JWT bis Rate Limiting

Easton editorial illustration: cache waterfall instrument

Das Handy vibriert – Rechnungserinnerung vom Cloud-Anbieter: 7.800 Dollar.

Ich reibe mir die Augen, glaube, ich habe mich verlesen. Letzten Monat waren es noch 120 Dollar. In den Details: API-Aufrufe – 18 Millionen. Mein persönliches Projekt macht normalerweise ein paar hundert Aufrufe pro Tag.

Ein Crawler hatte meinen völlig ungeschützten API-Endpunkt gefunden und drei Tage lang durchgerattert. In dem Moment wurde klar, warum alle sagen: API-Sicherheit ist keine Option.

Dieser Artikel fasst die Stolpersteine und Lösungen der letzten Jahre zusammen. Von JWT-Authentifizierung über CORS bis Rate Limiting und Eingabevalidierung – keine leere Theorie, sondern Code, den Sie direkt im Projekt nutzen können.

Warum API-Sicherheit so wichtig ist

Häufige Bedrohungen für APIs

10.0
CVSS-Score
Schwere React Server Components-Lücke (CVE-2025-55182) im Dezember 2025 – Angreifer können beliebigen Code ausführen

Im Dezember veröffentlichte React eine kritische Sicherheitswarnung: CVE-2025-55182, CVSS-Score 10.0.

Das bedeutet: volle Punktzahl. Mit einem speziell konstruierten HTTP-Request kann ein Angreifer beliebigen Code auf Ihrem Server ausführen. Wer React Server Components nutzt und nicht aktualisiert hat, läuft praktisch ungeschützt.

Das war kein Einzelfall. Im März folgte eine Autorisierungsumgehung (CVE-2025-29927), Score 9.1. Ein gefälschter Request-Header reicht, um Middleware-Auth zu umgehen. Sie dachten, Authentifizierung schützt? Der Angreifer springt einfach drüber.

Neben solchen Hochrisiko-Lücken gibt es im Alltag mehr Bedrohungen:

Bösartige Crawler und DDoS-Angriffe. Ohne Rate Limiting kann eine API in Minuten überlastet werden. Login-Endpunkte mit 3.000 Requests pro Sekunde – Brute-Force, Server down.

Datenlecks. Ohne saubere Berechtigungen sieht Nutzer A die Bestellungen von Nutzer B. Wenn das in den Nachrichten landet, ist der Ruf oft dahin.

Injection-Angriffe. SQL-Injection, XSS, Command Injection – altbekannt, aber jedes Jahr treffen unzählige Projekte. „React escaped automatisch“ hilft nicht, wenn die API nichts validiert.

Besonderheiten von Next.js API Routes

Next.js API Routes unterscheiden sich vom klassischen Backend – einige Punkte sind wichtig:

Serverless-first. Auf Vercel ist jeder API-Request eine eigene Serverless-Funktion. Vorteil: automatische Skalierung. Nachteil: zustandslos – klassische In-Memory-Sessions gehen nicht; JWT oder DB-Session nötig.

Frontend und Backend im selben Repo. Umgebungsvariablen können leicht ins Client-Bundle rutschen. DATABASE_URL in .env – und nach dem Build im Frontend-Bundle auf GitHub.

Edge Runtime. Im Edge Runtime fehlen manche Node.js-APIs; Crypto und DB-Verbindungen müssen neu gewählt werden – Sicherheitskonzepte mit anpassen.

Kurz: Next.js API ist Ihr Backend – leichter, flexibler, aber auch fehleranfälliger.

API-Authentifizierung in der Praxis

Leitfaden zur Wahl der Auth-Methode

Die praktische Frage: JWT oder Session?

Am Anfang meiner Next.js-Projekte habe ich das auch hin und her gewogen. JWT als moderner Standard vs. Session als sicherer – am Ende zählt das Szenario.

JWT passt, wenn:

  • die App auf mehreren Servern läuft (Serverless, Edge)
  • Cross-Domain-Auth nötig ist (Frontend app.com, API api.com)
  • Sie keine Session-Speicherung verwalten wollen

JWT kodiert Nutzerinfos in einem Token; der Server speichert keinen Zustand – jede Anfrage bringt den Token mit. Horizontal skalieren wird einfach.

Session passt, wenn:

  • der Server aktiv eingreifen soll (Abmelden, Rechte live ändern)
  • Sicherheit maximal ist und der Client keine Nutzerdaten halten soll
  • Redis oder DB für Sessions ohnehin da sind

Session: Zustand auf dem Server, Client nur Session-ID. Nutzer invalidieren = Session löschen – bei JWT schwieriger.

Meine Faustregel: Kleine/persönliche Projekte → JWT, weniger Aufwand; Enterprise mit feiner Kontrolle → Session. Nicht ewig debattieren – eine Variante wählen, bei Bedarf wechseln.

JWT-Authentifizierung vollständig implementieren

Sie entscheiden sich für JWT – wie setzen Sie es in Next.js um?

Schritt 1: Token erzeugen und verifizieren

Bibliothek installieren:

npm install jose

Warum nicht jsonwebtoken? Kein Edge Runtime. jose folgt Web-Standards und läuft überall.

lib/auth.ts anlegen:

import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(
  process.env.JWT_SECRET || 'your-secret-key-at-least-32-characters'
);

export async function createToken(payload: { userId: string }) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m') // Läuft nach 15 Minuten ab
    .sign(secret);
}

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch {
    return null;
  }
}

Die 15m-Laufzeit: Viele setzen 7 oder 30 Tage – bequem, aber bei Leak nutzt der Angreifer lange mit. Kurzer Access Token + langer Refresh Token ist der richtige Weg.

Schritt 2: Token speichern – nicht localStorage

Viele Tutorials speichern Token in localStorage – bei XSS sind sie weg.

Richtig: HttpOnly Cookie.

Login-Endpunkt:

// app/api/login/route.ts
import { NextResponse } from 'next/server';
import { createToken } from '@/lib/auth';

export async function POST(request: Request) {
  // Benutzername und Passwort prüfen...

  const token = await createToken({ userId: user.id });

  const response = NextResponse.json({ success: true });
  response.cookies.set('token', token, {
    httpOnly: true,    // JS kann nicht lesen – Schutz vor XSS
    secure: true,      // Nur über HTTPS
    sameSite: 'lax',   // Schutz vor CSRF
    maxAge: 900,       // 15 Minuten, entspricht Token-Ablauf
  });

  return response;
}

HttpOnly: JavaScript liest dieses Cookie nicht – XSS kann es nicht stehlen.

Schritt 3: Middleware schützt APIs

Token ist da – wie schützen Sie Routen, die Login brauchen? Next.js Middleware.

middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const payload = await verifyToken(token);
  if (!payload) {
    return NextResponse.json(
      { error: 'Invalid token' },
      { status: 401 }
    );
  }

  // Verifizierung OK – Nutzerinfo an API weitergeben
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-id', payload.userId as string);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: '/api/protected/:path*',
};

Alle /api/protected/*-Routen sind geschützt. In der API:

// app/api/protected/profile/route.ts
import { headers } from 'next/headers';

export async function GET() {
  const headersList = await headers();
  const userId = headersList.get('x-user-id');

  // Nutzerdaten aus der Datenbank laden...
}

Schritt 4: Token-Refresh

Nach 15 Minuten ständig neu einloggen? Refresh Token.

Access Token kurz (15 Min.), Refresh Token lang (30 Tage). Access abgelaufen → mit Refresh neuen holen, ohne Login.

Etwas mehr Implementierung – next-auth u. ä. haben das eingebaut.

Schnellintegration mit NextAuth.js

Die eigene Implementierung lehrt das Prinzip – viel Aufwand. Für schnellen Start: NextAuth.js (Auth.js).

Sichere Defaults out of the box:

  • CSRF-Schutz
  • Session-Signatur und Verschlüsselung
  • JWT und DB-Session
  • Google, GitHub u. a.

Installation:

npm install next-auth

app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials) {
        // Benutzername und Passwort prüfen...
        if (user) {
          return { id: user.id, email: user.email };
        }
        return null;
      }
    })
  ],
  session: {
    strategy: 'jwt',  // JWT – geeignet für Serverless
    maxAge: 30 * 24 * 60 * 60, // 30 Tage
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.userId = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      session.userId = token.userId;
      return session;
    }
  }
});

export { handler as GET, handler as POST };

Login in der API prüfen:

import { getServerSession } from 'next-auth';

export async function GET() {
  const session = await getServerSession();

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Eingeloggt – Request weiterverarbeiten...
}

NextAuth übernimmt Token und Session-Refresh.

CORS-Konfiguration im Detail

Was CORS ist und typische Probleme

CORS (Cross-Origin Resource Sharing) frustriert viele. Lokal läuft alles, nach dem Deploy:

Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.

Der Browser erlaubt Seite A nicht, Ressourcen von B zu laden. Von app.com zu api.com fragt der Browser: „Darf app.com?“ Die API muss explizit zustimmen.

Warum lokal kein CORS-Fehler?

Frontend und API auf localhost:3000 – same origin. In Produktion verschiedene Domains → Cross-Origin.

Was ist Preflight?

POST mit Custom-Header (z. B. Authorization) → Browser sendet zuerst OPTIONS. Ohne OPTIONS-Handler: 404, CORS scheitert.

Klassischer Fehler: POST fertig, OPTIONS vergessen – stundenlang CORS-Fehler.

Drei Wege, CORS in Next.js zu konfigurieren

Methode 1: next.config.js global

Wenn alle APIs dieselben Origins erlauben.

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
        ],
      },
    ];
  },
};

Vorteil: einmal konfiguriert. Nachteil: wenig flexibel pro Route.

Methode 2: Middleware

Für dynamische Entscheidungen zentral.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Preflight-Request behandeln
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': 'https://app.example.com',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      },
    });
  }

  // Normale Anfrage – CORS-Header setzen
  const response = NextResponse.next();
  response.headers.set('Access-Control-Allow-Origin', 'https://app.example.com');

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

Mit request können Sie Origins dynamisch prüfen.

Methode 3: In der API Route

Für einzelne Sonderfälle.

// app/api/public/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const data = { message: 'Hello' };

  return NextResponse.json(data, {
    headers: {
      'Access-Control-Allow-Origin': '*', // Öffentliche API – alle Origins erlaubt
    },
  });
}

export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
}

Jede Route braucht OPTIONS für Preflight.

CORS Best Practices

1. Wildcard sparsam nutzen

'Access-Control-Allow-Origin': '*'

Jede Website darf Ihre API aufrufen. Für öffentliche Daten ok – bei Nutzerdaten und sensiblen Aktionen riskant.

Besser: explizite Domains.

const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];

const origin = request.headers.get('origin');
if (origin && allowedOrigins.includes(origin)) {
  response.headers.set('Access-Control-Allow-Origin', origin);
}

2. Credentials vorsichtig

API liest Cookies (Session)? Frontend:

fetch('https://api.example.com', {
  credentials: 'include',
});

Backend:

response.headers.set('Access-Control-Allow-Credentials', 'true');

Access-Control-Allow-Origin darf dann nicht * sein – Browser lehnt ab; konkrete Domain nötig.

3. Preflight behandeln

Requests mit Authorization oder Content-Type: application/json triggern meist Preflight – OPTIONS implementieren.

Hilfsfunktion:

export function corsHeaders(origin?: string) {
  return {
    'Access-Control-Allow-Origin': origin || 'https://app.example.com',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400', // Preflight-Ergebnis 24 Stunden cachen
  };
}

In APIs wiederverwenden.

API Rate Limiting

Warum Rate Limiting nötig ist

Zurück zur Rechnung: 18 Millionen Aufrufe. Mit Limit 100 pro IP und Minute wären es vielleicht ein paar Dollar statt 7.800.

Rate Limiting begrenzt API-Aufrufe pro Zeitraum:

DDoS. Massenrequests? Über Schwellwert → abgelehnt, Server stabil.

Brute-Force. Login ohne Limit: tausende Passwörter pro Sekunde. Mit Limit z. B. 5 pro Minute steigt der Aufwand stark.

Ressourcenschutz. DB und Drittanbieter kosten Geld – ein Nutzer soll nicht alles verbrauchen.

Vergleich der Ansätze

Option 1: @upstash/ratelimit + Vercel KV

Mein Standard. Upstash = Serverless Redis, Vercel-Partner.

Vorteile:

  • Serverless-tauglich
  • Festes Fenster, Sliding Window, Token Bucket
  • Free Tier für persönliche Projekte

Nachteile:

  • Kosten bei hohem Traffic
  • Drittanbieter-Abhängigkeit

Option 2: Self-hosted Redis

Eigene Redis-Instanz.

Vorteile:

  • Volle Kontrolle, keine Extra-Kosten
  • Individuelle Logik

Nachteile:

  • Betrieb
  • In Serverless aufwändiger

Option 3: In-Memory

Ohne Redis, einfaches Limit im Speicher.

Vorteile:

  • Keine Abhängigkeit, wenig Code
  • Dev und kleine Projekte

Nachteile:

  • Serverless: neue Instanz pro Request, kein geteilter Speicher
  • Nach Neustart weg

Empfehlung: Persönlich/Serverless → Upstash; Enterprise mit eigenem Server → Redis; Demo/lokal → In-Memory.

Praxis: Upstash

Schritt 1: Installation

npm install @upstash/ratelimit @upstash/redis

Redis bei Upstash anlegen, UPSTASH_REDIS_REST_URL und UPSTASH_REDIS_REST_TOKEN in .env:

UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token

Schritt 2: Limiter

lib/rate-limit.ts:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// Redis-Client erstellen
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// Rate Limiter: Sliding Window, max. 10 Requests in 10 Sekunden
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
});

slidingWindow(10, '10 s'): max. 10 Requests in 10 Sekunden – glatter als festes Fenster.

Schritt 3: In der API

// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';

export async function GET(request: Request) {
  // Client-IP ermitteln
  const ip = request.headers.get('x-forwarded-for') || 'unknown';

  // Rate Limit prüfen
  const { success, limit, remaining, reset } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      {
        error: 'Too many requests',
        limit,
        remaining,
        reset: new Date(reset),
      },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        },
      }
    );
  }

  // Limit OK – Request normal verarbeiten
  return NextResponse.json({ data: 'Success' });
}

Mit Login: userId statt nur IP:

const identifier = session?.userId || ip;
const { success } = await ratelimit.limit(identifier);

Schritt 4: Global in Middleware

// middleware.ts
import { ratelimit } from '@/lib/rate-limit';

export async function middleware(request: NextRequest) {
  const ip = request.ip || 'unknown';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

Alle /api/*-Routen geschützt.

Eingabevalidierung und Abwehr

Warum Validierung die erste Linie ist

„Niemals Nutzereingaben vertrauen“ – Grundregel der Sicherheit.

Frontend-Validierung? DevTools, Code ändern – umgangen. Echte Abwehr auf dem Server.

SQL-Injection. '; DROP TABLE users; -- in SQL konkateniert → Datenbank weg. ORMs helfen, natives SQL bleibt riskant.

XSS. <script>alert('hacked')</script> gespeichert → andere Nutzer, Script läuft, Cookies weg. React escaped – mit dangerouslySetInnerHTML nicht.

DoS. 10-MB-JSON → Serverless-Speicher voll. Oder sehr lange Strings → Regex-Katastrophe.

Validierung stoppt die meisten einfachen Angriffe. Ohne sie sind weitere Maßnahmen löchrig.

Typsichere Validierung mit Zod

Zod passt gut zu TypeScript.

npm install zod

Grundlagen

Schema:

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  age: z.number().int().min(18).max(120),
});

In der API:

// app/api/register/route.ts
import { NextResponse } from 'next/server';
import { userSchema } from '@/lib/schemas';

export async function POST(request: Request) {
  const body = await request.json();

  // Daten validieren
  const result = userSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json(
      {
        error: 'Validation failed',
        details: result.error.format(),
      },
      { status: 400 }
    );
  }

  // Validierung OK – typsichere Daten
  const { email, password, age } = result.data;

  // Weiterverarbeiten...
}

safeParse wirft nicht – anders als parse mit try-catch.

Vorteil gegenüber Hand-Checks

Hand:

if (!body.email || typeof body.email !== 'string') {
  return error;
}
if (!body.email.includes('@')) {
  return error;
}
// Endlose manuelle Checks...

Zod:

z.string().email()

Eine Zeile, Typen abgeleitet.

Vollständiges Validierungsschema

Nicht nur Typen – Business-Logik und Grenzfälle.

1. Request Body

const postSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().max(10000), // Länge begrenzen – Schutz vor Rieseneingaben
  tags: z.array(z.string()).max(10), // Array-Länge begrenzen
  publishedAt: z.string().datetime().optional(),
});

2. Query-Parameter

// app/api/posts/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  const querySchema = z.object({
    page: z.coerce.number().int().min(1).default(1),
    limit: z.coerce.number().int().min(1).max(100).default(20),
    sort: z.enum(['asc', 'desc']).default('desc'),
  });

  const params = querySchema.parse({
    page: searchParams.get('page'),
    limit: searchParams.get('limit'),
    sort: searchParams.get('sort'),
  });

  // params.page ist garantiert eine Zahl – typsicher
}

z.coerce.number() wandelt Strings in Zahlen.

3. Eigene Regeln

const passwordSchema = z.string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), 'Must contain uppercase')
  .refine((val) => /[a-z]/.test(val), 'Must contain lowercase')
  .refine((val) => /[0-9]/.test(val), 'Must contain number');

Asynchron:

const emailSchema = z.string().email().refine(
  async (email) => {
    const exists = await checkEmailExists(email);
    return !exists;
  },
  'Email already taken'
);

4. Fehlerbehandlung

if (!result.success) {
  const errors = result.error.errors.map(err => ({
    field: err.path.join('.'),
    message: err.message,
  }));

  return NextResponse.json({ errors }, { status: 400 });
}

Strukturierte Fehler für das Frontend.

Weitere Sicherheitsmaßnahmen

1. CSRF-Schutz

Server Actions in Next.js vergleichen Origin und Host. API Routes selbst bauen – NextAuth hilft; manuell CSRF-Token (Cookie + Request vergleichen).

2. Content Security Policy (CSP)

In next.config.js:

{
  headers: [
    {
      key: 'Content-Security-Policy',
      value: "default-src 'self'; script-src 'self'; style-src 'self';",
    },
  ],
}

Bei XSS laden bösartige Scripts nicht.

3. Umgebungsvariablen

  • NEXT_PUBLIC_* → Client sichtbar
  • ohne Präfix → nur Server

Keine Secrets in NEXT_PUBLIC_. NEXT_PUBLIC_API_KEY = Leak.

4. SQL-Injection

Prisma, Drizzle – parametrisiert. Natives SQL:

// ❌ Gefährlich
db.query(`SELECT * FROM users WHERE id = ${userId}`);

// ✅ Sicher
db.query('SELECT * FROM users WHERE id = ?', [userId]);

5. Dependencies aktuell halten

npm audit
npm update

Die React-Lücke im Februar? Update auf Latest – weniger Ärger als nach einem Incident.

Vollständige Sicherheits-Checkliste

Authentifizierung

  • ✅ Token in HttpOnly Cookie, nicht localStorage
  • ✅ Access Token ≤ 30 Minuten
  • ✅ Refresh-Token-Mechanismus
  • ✅ JWT-Secret min. 32 Zeichen, in Env
  • ✅ Cookie secure und sameSite
  • ✅ Middleware für sensible APIs

CORS

  • ✅ Kein Access-Control-Allow-Origin: * für sensible APIs
  • ✅ Explizite Domain-Liste
  • ✅ OPTIONS Preflight
  • ✅ Bei Credentials: Access-Control-Allow-Credentials: true
  • ✅ CORS in Produktion testen

Rate Limiting

  • ✅ Striktes Limit für Login, Registrierung, Passwort-Reset
  • ✅ Unterschiedliche Limits für authentifiziert/nicht authentifiziert
  • ✅ Status 429 und Retry-After
  • ✅ Redis/Upstash statt nur In-Memory in Serverless
  • ✅ Limits überwachen und anpassen

Eingabevalidierung

  • ✅ Alle Eingaben serverseitig
  • ✅ Zod o. ä.
  • ✅ Max-Längen für String, Array, Objekt
  • ✅ Formate (E-Mail, URL, Datum)
  • ✅ Klare Fehlermeldungen

Regelmäßiges Audit

  • ✅ Monatlich npm audit, High/Critical fixen
  • ✅ Next.js und React aktuell
  • ✅ Security Advisories abonnieren
  • ✅ Env-Variablen prüfen – keine Secrets im Client
  • ✅ Code Review: Auth und Berechtigungen

Checkliste vor Projektstart und vor Deploy durchgehen.

Fazit

Kernbotschaft: API-Sicherheit ist Systemarbeit, kein einmaliges Setup.

Auth, CORS, Rate Limiting, Validierung – Aufwand. Nach Leak, Ausfall oder Rechnungsschock ist Nachbessern teurer.

Meine Erfahrung: Erstes Setup etwa halber bis ganzer Tag. Danach Copy-Paste mit Anpassungen – neues Projekt in Minuten. Und ruhiger schlafen.

Handlungsempfehlungen:

  1. Bestehendes Projekt prüfen – Checkliste
  2. Mit Kritischem starten – Auth und Rate Limiting zuerst
  3. Security-News folgen – Next.js Blog, GitHub Advisories
  4. Team einbinden – Sicherheit ist gemeinsame Aufgabe

Noch einmal: CVE-2025-55182 (CVSS 10.0) im Dezember 2025 – große Reichweite. Nicht aktualisiert? Jetzt upgraden. Security-Updates nicht aufschieben.

Der Weg zur API-Sicherheit ist lang – jeder Schritt lohnt sich. Hoffentlich sparen Sie sich einige der Stolpersteine aus diesem Artikel.

FAQ

Sollte eine Next.js API JWT oder Session-Authentifizierung verwenden?
Das hängt vom Szenario ab. JWT eignet sich für Serverless-Deployments und Cross-Domain-Auth – zustandslos und gut skalierbar. Session passt, wenn der Server aktiv eingreifen soll (z. B. Nutzer abmelden) oder Sicherheitsanforderungen sehr hoch sind. Für persönliche Projekte oft JWT; für Enterprise-Anwendungen eher Session.
Warum sollte man JWT-Token nicht in localStorage speichern?
localStorage ist per JavaScript lesbar – bei erfolgreichem XSS-Angriff können Token gestohlen werden. Besser: HttpOnly-Cookies. JavaScript kann diese nicht lesen; selbst bei XSS-Lücken bleiben Token geschützt.
Warum gibt es in der Entwicklung keine CORS-Fehler, in Produktion aber schon?
Lokal laufen Frontend und API auf localhost:3000 – same origin, kein Cross-Origin. In Produktion können Frontend und API unterschiedliche Domains haben; der Browser prüft CORS. Lösung: Access-Control-Allow-Origin auf der API-Seite korrekt setzen.
Wie implementiert man Rate Limiting in Serverless-Umgebungen?
Empfohlen: @upstash/ratelimit + Vercel KV. Jede Serverless-Anfrage kann eine neue Instanz sein – In-Memory-Limitierung teilt keinen Speicher. Upstash Redis bietet persistente Speicherung; alle Instanzen teilen Limit-Daten – ideal für Serverless.
Wie schützt man APIs vor Passwort-Brute-Force?
Mehrschichtiger Schutz: 1) Striktes Rate Limiting am Login (z. B. max. 5 Versuche pro IP und Minute); 2) Passwortstärke mit Zod prüfen; 3) Kontosperre (z. B. nach 5 Fehlversuchen 30 Minuten Sperre); 4) CAPTCHA erhöht Angriffskosten.

13 Min. Lesezeit · Veröffentlicht am: 5. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog