Warum Next.js Server Actions eine Autorisierung innerhalb des Körpers jeder Funktion benötigen
Ein detaillierter Fallbeschreibung einer Account-Übernahme zeigt, wie unauthentifizierte Next.js Server Actions privilegierte Operationen ermöglichen und wo die Autorisierungsprüfung angeordnet werden muss, um dies zu verhindern.
Quellen: Unauthenticated Next.js Server Actions Leading to Zero Click Account Takeover
Next.js Server Actions ermöglichen es einer React-Komponente, direkt eine Serverfunktion aufzurufen, ohne dass dabei eine API-Route dazwischenliegt. Diese Bequemlichkeit beseitigt den Ort, an dem viele Teams üblicherweise eine Autorisierungsprüfung durchführen: den Route-Handler. Wenn die Prüfung nicht zusammen mit der Logik in den Funktionskörper übertragen wird, kann die Funktion von jedem aufgerufen werden, der ihren Identifikator finden kann. Der folgende Fall zeigt genau dieses Versagen in einem echten Abonnementportal, wo zwei ungeschützte Aktionen zu einer klickfreien Kontovernahme führten, und erläutert anschließend die defensiven Regeln, die dies verhindert hätten.
Wie Server Actions tatsächlich ausgeführt werden
Im App Router ist eine Server Action eine Serverfunktion, die direkt von einer Komponente aufgerufen wird. Das Framework weist jeder Action eine 40-stellige hexadezimale ID zu, packt diese ID in den Client-Bundle ein und wandelt den Aufruf in eine POST-Anfrage um, die ein Next-Action-Header enthält. Der entscheidende Punkt ist, dass die Auslösung anhand dieses Headers und nicht anhand des URL-Pfads erfolgt, wobei der Anfragekörper einfach die argumentativ übergebenen Parameter der Funktion ist.
Dieses Design hat eine Folge, die leicht übersehen werden kann. Es gibt kein Route-File, das in Middleware eingebettet werden könnte, sodass es keinen natürlichen Engpass gibt, an dem ein Schutzmechanismus auf Framework-Ebene vor Ihrem Code ausgeführt wird. Wenn die Autorisierungsprüfung nicht direkt innerhalb der Funktion selbst implementiert ist, findet überhaupt keine Prüfung statt. Jeder, der die Action-IDs aus dem bereitgestellten Bundle extrahiert und die Argumentstrukturen anhand der minifizierten Aufrufstellen rekonstruiert, kann die Anfrage ohne angehängtes Session-Cookie erneut senden.
Die erste Aktion: Ein E-Mail-Eingang, ein privilegiertes Datensatz-Ausgang
Auf dem betreffenden Portal erreichte eine Anfrage ohne Cookies, ohne Autorisierungs-Header und ohne weitere Identifikationsmerkmale des Absenders die erste Aktion. Im Anfragekörper befanden sich lediglich eine E-Mail-Adresse und ein Boolescher Wert.
POST /particuliers/integrale-web-mensuel-2116 HTTP/2
Host: subdomain.target.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Next-Action: 42a24b711d12f695d798b371e38b584c551ebb6a
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
["victim@gmail.com",true]
Die Antwort enthielt den vollständigen Kontoinformationen-Record für diese E-Mail-Adresse.
{
"user_id": "auth0|000000000000000000000001",
"email": "researcher@example.com",
"identities": [
{ "provider": "auth0", "connection": "www-target-com" }
],
"app_metadata": {
"subscriber_number": "900000000",
"custom_user_id": "00000000–0000–4000–8000–000000000000"
}
}
Die Struktur dieses Payloads ist der Hinweis. Felder wie identities und app_metadata werden nicht über die öffentliche Authentifizierungs-API eines Identitätsanbieters bereitgestellt; sie stammen vielmehr aus seiner Management-API, der administrativen Schnittstelle, die mit einem Machine-to-Machine-Token mit Berechtigungen wie Lesen und Aktualisieren von Benutzern erreicht wird. Wenn diese Daten über einen unauthentifizierten Aufruf zurückgegeben werden, bedeutet das, dass ein berechtigtes Token auf dem Server lag und eine anonyme Funktion es heimlich verwendet hat. Neben dieser Erkenntnis enthielt die Antwort auch zwei Werte, die für die nächste Aktion benötigt wurden: eine Benutzer-ID und einen Verbindungsnamen.
Die zweite Aktion: Das Passwort eines Benutzers setzen
Die zweite Aktion nahm ein Objekt und eine Zeichenkette entgegen. Das Objekt enthielt ein Passwort sowie eine Verbindung; die Zeichenkette war eine Benutzer-ID. Bei einem Testkonto, das immer noch nicht authentifiziert war, sah der Aufruf so aus.
POST / HTTP/2
Host: subdomain.target.com
Next-Action: 89abcdef0123456789abcdef0123456789abcdef
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
[{"password":"<REDACTED>","connection":"www-target-com"},"auth0|000000000000000000000001"]
Die Anfrage bewies nichts: kein Session-Cookie, keine Kenntnis des bestehenden Passworts, kein Reset-Token oder E-Mail-Bestätigung sowie keine Aufforderung zur zusätzlichen Überprüfung. Der Server antwortete mit Erfolg und dem aktualisierten Benutzerobjekt. Der entscheidende Fehler liegt darin, woher die Benutzer-ID stammt: Sie ist das zweite Argument, das vom Aufrufer übergeben wird, anstatt aus einer Session oder dem Subject-Claim eines signierten Tokens gelesen zu werden. Die Funktion ändert das Konto, für das ihr gesagt wird.
Dieselbe Aktion, ein anderes Feld – eine entführte Wiederherstellungsroute
Weil das erste Argument direkt an den Benutzer-Update-Dienst des Identitätsanbieters weitergeleitet wird, ist das Feld, das Sie darin platzieren, dasjenige, das geändert wird. Wenn man das Passwort durch eine E-Mail-Adresse ersetzt, wird stattdessen die E-Mail-Adresse des Kontos aktualisiert.
POST / HTTP/2
Host: subscribe.target.com
Next-Action: 89abcdef0123456789abcdef0123456789abcdef
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
[{"email":"attacker@example.com","connection":"www-target-com"},"auth0|000000000000000000000001"]
Das Ergebnis war derselbe Erfolg – mit derselben Abwesenheit jeglicher Eigentumsprüfung. Diese Variante ist schlimmer, als sie auf den ersten Blick erscheint: Die Adresse eines Kontos ist der Ort, an dem Passwort-Reset-E-Mails zugestellt werden. Sobald sie einem Angreifer gehört, führt der Wiederherstellungsprozess des echten Besitzers nicht mehr zu ihm zurück. Da das Passwort direkt gesetzt wird anstelle einer Sitzung ausgeliehen zu werden, läuft der dadurch entstandene Zugriff nicht von selbst ab.
Die Lektionen, die dieser Fall erteilt
Server-Aktionen sind Backend-Endpunkte, die als Funktionen getarnt sind, und verdienen daher denselben Misstrauensgrad. Führen Sie eine Autorisierungsprüfung in jeder Aktion durch, die auf privilegierte Daten oder Zustände zugreift, und überprüfen Sie den Aufrufer vor jeder weiteren Aktion. Ermitteln Sie die Identität des handelnden Benutzers aus der Sitzung oder einem verifizierten Token – niemals aus einem Argument, das vom Client gesteuert wird – damit eine Anfrage nicht die ID eines Opfers angeben und den Server dazu bringen kann, dieser zu folgen. Halten Sie privilegierte Provider-Tokens von Codepfaden fern, die ohne Authentifizierung erreichbar sind. Behandeln Sie Aktion IDs als öffentlich, da sie an den Browser übermittelt werden; Verschleierung ist kein Zugriffskontrollmechanismus. Jede Funktion, die ein Client aufrufen kann, ist auch eine Funktion, die ein Angreifer aufrufen kann – das Einzige, was zwischen ihnen und Ihren Daten steht, ist die Prüfung, die Sie im Code implementiert haben.