Next.js Server Actions legen automatisch jede exportierte Funktion in einer use-server-Datei als öffentlichen HTTP-Endpunkt offen, indem sie ihr eine eindeutige Kennung zuweisen, die der Client verwendet, um die Funktion aufzurufen. Der Endpunkt existiert, unabhängig davon, ob ein Button, ein Link oder ein anderes UI-Element gerendert wird. Das bedeutet, dass jeder, der die Kennung entdeckt, die Funktion direkt aufrufen kann.
Das Design des Frameworks behandelt Server Actions wie gewöhnliche Helper-Funktionen, aber zur Laufzeit werden sie zu erreichbaren URLs. Für Entwickler, die davon ausgingen, dass die UI der einzige Gatekeeper ist, entsteht so eine unsichtbare Angriffsfläche, die mit einer einzigen manipulierten Anfrage ausgenutzt werden kann.
Warum Server Actions sicher schienen
Server Actions wurden eingeführt, damit Entwickler serverseitigen Code direkt neben ihren Komponenten schreiben können, um den Boilerplate-Aufwand separater API-Routen zu vermeiden. Eine typische Verwendung sieht so aus:
<form action={deleteInvoice}>
<button type="submit">Delete</button>
</form>
Da das Formular der einzige sichtbare Weg ist, um deleteInvoice auszulösen, verstecken viele Entwickler den Button für nicht autorisierte Benutzer, glauben, dass TypeScript-Signaturen malformierte Daten verhindern, und verlassen sich darauf, dass die Funktion in einem Server-only-Modul liegt. Keine dieser Annahmen bietet echten Schutz.
Die versteckte Offenlegung
Wenn eine Datei „use server“ enthält, kompiliert Next.js jede exportierte Funktion in einen Endpunkt wie zum Beispiel:
POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>
Die <action-id> ist ein stabiler Hash, der in das Client-Bundle eingebettet wird. Ein Angreifer kann ihn folgendermaßen erhalten:
- Durch das Inspizieren des Netzwerkverkehrs der Seite.
- Durch das Lesen des gebündelten JavaScripts (die ID ist ein einfacher String).
- Durch Raten basierend auf Namenskonventionen, falls das Projekt vorhersehbaren Mustern folgt.
Sobald die ID bekannt ist, kann eine Anfrage von jedem beliebigen Tool gesendet werden – cURL, Postman oder ein bösartiges Skript –, wodurch alle Prüfungen auf UI-Ebene umgangen werden.
Drei konkrete Risiken
| Risiko | Warum es wichtig ist |
|---|---|
| UI-Prüfungen sind unwirksam | Das Ausblenden eines Buttons oder Links löscht nicht den zugrunde liegenden Endpunkt. Der Endpunkt bleibt erreichbar, genau wie eine versteckte Admin-Seite, die auf dem Server weiterhin existiert. |
| TypeScript bietet keine Laufzeitsicherheit | Typen werden beim Ausführen des Codes entfernt. Eine als deleteInvoice(id: number) deklarierte Funktion kann einen riesigen String, ein Array oder sogar bösartiges JSON erhalten, was zu Logikfehlern oder Injection-Angriffen führen kann. |
| Keine standardmäßige Authentifizierung oder Autorisierung | Server Actions sehen aus wie lokale Helper, daher vergessen Entwickler oft, Sitzungsprüfungen, CSRF-Schutz oder Berechtigungsprüfungen auf Zeilenebene hinzuzufügen, die in traditionellen API-Routen Standard sind. |
So sichern Sie eine Server Action ab
- Authentifizieren Sie den Aufrufer – Überprüfen Sie, ob eine gültige Sitzung oder ein Token existiert, bevor die Geschäftslogik ausgeführt wird.
- Validieren Sie die Payload – Verwenden Sie eine Schema-Bibliothek (z. B. Zod, Yup), um Datentypen und Wertbeschränkungen zur Laufzeit zu erzwingen.
- Autorisieren Sie die Operation – Bestätigen Sie über die Frage „Ist der Benutzer eingeloggt?“ hinaus, dass der Benutzer der spezifische Datensatz ist, den er zu ändern oder zu löschen versucht.
Ein minimales Beispiel:
'use server';
import { getSession } from '@/auth';
import { z } from 'zod';
import { db } from '@/db';
const DeleteInvoiceSchema = z.object({
id: z.number().int().positive(),
});
export async function deleteInvoice(formData: FormData) {
const session = await getSession();
if (!session) throw new Error('Unauthenticated');
const parsed = DeleteInvoiceSchema.safeParse({
id: Number(formData.get('id')),
});
if (!parsed.success) throw new Error('Invalid input');
const invoice = await db.invoice.findUnique({ where: { id: parsed.data.id } });
if (!invoice || invoice.ownerId !== session.userId) {
throw new Error('Unauthorized');
}
await db.invoice.delete({ where: { id: invoice.id } });
}
Der Code prüft explizit die Authentifizierung, validiert die eingehende id und stellt sicher, dass der angemeldete Benutzer tatsächlich der Besitzer der Rechnung ist, bevor der Löschvorgang durchgeführt wird.
Weitere subtile Next.js-Fallstricke
| Problem | Symptom | Lösung |
|---|---|---|
NEXT_PUBLIC_ Umgebungsvariablen |
Alles, was mit NEXT_PUBLIC_ beginnt, wird in den Client gebündelt, wodurch Geheimnisse offengelegt werden. |
Bewahren Sie Geheimnisse in normalen Umgebungsvariablen auf und verwenden Sie niemals das Präfix NEXT_PUBLIC_. |
| Open Redirects | Das Akzeptieren eines redirect-Abfrageparameters und dessen naive Verkettung kann Benutzer zu //evil.com weiterleiten. |
Validieren Sie das Ziel gegen eine Whitelist oder erzwingen Sie Same-Origin-Prüfungen. |
| Server-Side Request Forgery (SSRF) | Das Abrufen einer vom Benutzer bereitgestellten URL kann es Angreifern ermöglichen, interne Dienste oder Cloud-Metadaten-Endpunkte zu erreichen. | Erstellen Sie eine Allowlist für Hostnamen, blockieren Sie private IP-Bereiche und legen Sie Timeouts fest. |
dangerouslySetInnerHTML |
Das Rendern von benutzergesteuertem HTML ohne Bereinigung (Sanitization) ermöglicht XSS. | Verwenden Sie eine Bibliothek wie DOMPurify oder vermeiden Sie rohes HTML ganz. |
Automatisches Scannen nach diesen Mustern
Statische Analysewerkzeuge können die oben aufgeführten riskanten Konstrukte markieren. Eine leichtgewichtige Option ist Semgrep, das schnell läuft und in CI-Pipelines integriert werden kann:
npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .
Der Regelsatz enthält Prüfungen für exportierte Server-Funktionen, den Missbrauch von NEXT_PUBLIC_, Open Redirects, SSRF-Muster und unsicheres Einfügen von HTML.
Was als Nächstes zu beachten ist
- Framework updates – Keep an eye on Next.js releases; the team may introduce built-in authentication hooks or sandbox the generated endpoints.
- Community tooling – New ESLint plugins and Next.js-specific Semgrep rules are emerging to automate the safeguards described here.
- Real-world incidents – As more projects adopt Server Actions, watch for disclosed exploits that illustrate the risk in practice. Early detection can inform internal security reviews before a breach occurs.
Takeaway: A Next.js Server Action is not a private helper; it is a public HTTP endpoint the moment you export it. Treat it like any other API route—authenticate, validate, and authorize—otherwise the convenience of writing server code next to UI can quickly become a security liability.
