Transcript
00:00:00Es ist wieder passiert. Das ist bereits mein drittes Video zu Server-Component-CVEs in diesem Jahr,
00:00:05und ich glaube nicht mal, dass ich alle abgedeckt habe. Diesmal sind es 13 CVEs bei React
00:00:11und Next.js – ja, 13 Stück –, wovon 6 als hochriskant eingestuft sind, darunter Denial-of-Service,
00:00:15Middleware-Bypasses, Cross-Site Scripting und mehr. Vielleicht waren Server Components ein Fehler.
00:00:20Hier ist das Next.js-Sicherheits-Release, in dem nur ein paar „beiläufige“ Probleme behoben werden,
00:00:28die sie diesen Monat hatten. Ganz unten steht natürlich die Lösung: Aktualisiert
00:00:32alle eure Next.js-Versionen. Das hier sind die betroffenen Versionen. Es ist erwähnenswert,
00:00:36dass TanStack davon nicht betroffen ist. Ich bin vielleicht voreingenommen, aber das ist ein weiterer Grund,
00:00:41warum ich TanStack nutze. Ich werde jetzt nicht alle durchgehen, sonst säßen wir ewig hier,
00:00:44und ich habe auch noch nicht für alle funktionierende Exploits gefunden. Aber ich möchte euch
00:00:48einen aus jeder Kategorie zeigen. Wir beginnen mit einem Middleware- und Proxy-Bypass,
00:00:52und den, den ich nachstellen konnte, ist dieser hier beim Pages Router. Wir haben also einen
00:00:56Middleware-Proxy-Bypass im Pages Router bei Verwendung von i18n. Dieser CVE hat einen Schweregrad
00:01:02von 7,5 von 10. Dies ist ein Beispiel für eine verwundbare Anwendung: In der Next.js-Konfiguration
00:01:06habe ich i18n aktiviert und zwei Locales eingerichtet: Englisch und Französisch. Zudem gibt es eine Middleware-Datei,
00:01:12die in neueren Next.js-Versionen in „proxy“ umbenannt wurde, um die Verwirrung zu vermeiden,
00:01:16die ich gleich zeige. Im Grunde sollte uns Middleware erlauben,
00:01:19eingehende Anfragen zu modifizieren, sei es durch Weiterleitung, Rewriting oder das Hinzufügen
00:01:24von Headern. In meinem Fall prüfe ich beim Versuch, die „/secret“-Seite aufzurufen,
00:01:28ob ein Session-Cookie vorhanden ist, der Nutzer also eingeloggt ist. Falls nicht,
00:01:32sollte die Weiterleitung zur Login-Seite erfolgen, damit nur autorisierte Nutzer mein Geheimnis sehen.
00:01:37Unten haben wir einen Matcher, damit die Middleware für die Geheimseite auch
00:01:41die lokalen Varianten erfasst, da wir durch die zwei Locales technisch gesehen drei Versionen der URL haben.
00:01:45Auf der Geheimseite selbst nutze ich Server-Side Props; diese sollten zum Rendering-Zeitpunkt
00:01:50vom Server abgerufen werden. Wegen der Middleware sollte theoretisch nur ein angemeldeter Nutzer
00:01:54diese Werte sehen können. Diese nutze ich später auf der Seite selbst: eine E-Mail, ein Flag
00:01:58und eine Überschrift. Nur ein autorisierter Nutzer sollte das sehen können. Testen wir das mal.
00:02:03Zuerst versuche ich, die Geheimseite aufzurufen. Wie man sieht, werde ich zum Login weitergeleitet,
00:02:07da ich nicht eingeloggt bin. Die Middleware funktioniert also. Aber was, wenn wir zu Meister-Hackern werden?
00:02:11Das können wir tun, indem wir zuerst das Element untersuchen – absolut krasses Hacker-Zeug.
00:02:16Im „next-data“-Skript hier unten müssen wir nach unserer Build-ID suchen. In meinem Fall
00:02:20ist es diese hier. Wir kopieren sie und müssen dann eine URL eingeben,
00:02:24die so aussieht: _next/data/ [Build-ID] / [Seite].json.
00:02:28Wenn man das macht, sieht man, dass wir die Props zurückbekommen, die eigentlich durch die
00:02:32Middleware geschützt sein sollten. In meinem Fall waren das Flag, E-Mail, Überschrift und der Hinweis,
00:02:37für mehr Entwickler-News und Tipps zu abonnieren. Also tut das bitte! Ich hoffe, ich habe euch
00:02:40mit meinen Hacker-Skills beeindruckt. Aber warum passiert das eigentlich? Nun, das ist
00:02:44so einfach zu erklären wie es durchzuführen war. Wir hatten unsere Geheimseite mit Server-Side Props.
00:02:48In Next.js werden diese Props über eine URL wie diese hier ausgeliefert. Unsere Middleware
00:02:52hätte diese Route schützen müssen. Das Problem: Da wir i18n nutzten, hatten wir
00:02:56auch zwei andere URLs, die englische und die französische Variante. Man sieht,
00:03:00dass auch die Server-Side Props englische und französische Varianten erhalten. Next.js hatte
00:03:05einfach fehlerhaften Code: Wenn i18n aktiviert war, wurde der Basisfall nicht geschützt.
00:03:09Dieser war nicht im Matcher enthalten, die englische und französische Version hingegen schon.
00:03:13Die Varianten waren geschützt, aber nicht der Basisfall „/secret“. Das sieht man sofort,
00:03:18wenn ich die URL zur englischen Version ändere: Ich werde zum Login weitergeleitet. Eine unglaublich
00:03:22einfache Sicherheitslücke. Aber ehrlich gesagt klingen diese Middleware-Bypasses oft schlimmer,
00:03:26als sie sind. Sie sind nicht gut, aber man sollte ohnehin nicht zu viel allein mit Middleware schützen.
00:03:31Next.js empfiehlt das auch gar nicht. Wenn ihr sensible Daten in Server-Side Props habt
00:03:35und keine Server-Auth-Logik nutzt, liegt ein Teil der Schuld auch bei euch. Kommen wir zu etwas
00:03:40Schwerwiegenderem: Denial-of-Service. Es gab drei davon, aber ich konnte nur einen
00:03:44zuverlässig rekonstruieren. Das war dieser hier: Denial-of-Service mit Server Components.
00:03:48Das betrifft Next.js sowie alles, was das „react-server-dom“-Paket nutzt – also fast nur Next.js
00:03:53und Frameworks, die es kopiert haben. TanStack Start nutzt das nicht und ist daher nicht anfällig.
00:03:56Die Schwere liegt auch hier bei 7,5 von 10. Für diesen Exploit reicht eine einfache Next.js-App,
00:04:01die eine Server Action verwendet. Hier läuft die Seite gerade, und wie man beim Refresh sieht,
00:04:05lädt sie fast sofort. Um das in Zahlen zu fassen: Wenn ich diese Anfrage sende,
00:04:10dauert es 0,02 Sekunden. Wenn ich nun meinen Exploit starte und die Anfrage erneut sende,
00:04:14dauert es sechs Sekunden. Und das war nur ein einziger Exploit-Durchlauf – stellt euch vor,
00:04:18ich würde sie verketten. Um den Exploit zu verstehen, muss man das React-Flight-Protokoll kennen.
00:04:22Das ist das Format, mit dem React Komponentenbäume und Daten zwischen Server und Client serialisiert.
00:04:25Ihr habt das wahrscheinlich schon mal gesehen: Auf dieser Seite hatten wir ein Formular mit einer Server Action.
00:04:29Im Network-Tab sieht man, dass der Payload als Daten gesendet wird, die wie Kauderwelsch aussehen.
00:04:34Dasselbe gilt für die Antwort. Wenn wir diesen Payload kopieren, kann ich erklären,
00:04:39was auf dem Server passiert. Der erste Schritt ist die Deserialisierung. Sie beginnt bei Chunk 0,
00:04:42wo wir dieses „$k1“ haben. Das ist ein Pointer, der besagt, dass hier Formular-Daten folgen,
00:04:46die mit einem Unterstrich beginnen. Er nimmt also alle anderen Keys des Payloads,
00:04:50geht sie durch und sucht nach einem String, der mit einem Unterstrich beginnt, um Key und Value zu finden.
00:04:54Danach kann er Name, E-Mail und Nachricht zuordnen und die Daten in dieses Objekt hier unten umwandeln.
00:04:58Schön einfach. Das Problem bei diesem Ansatz zeigt sich aber bei der Skalierung.
00:05:02Sagen wir, ich füge einen weiteren Pointer „$k2“ hinzu, der nach Keys sucht, die mit zwei Unterstrichen beginnen.
00:05:05Nun passiert folgendes: Bei „$k1“ werden alle sechs Keys nach einem Unterstrich durchsucht.
00:05:10Bei „$k2“ passiert genau dasselbe für zwei Unterstriche. Wir prüfen also insgesamt 12 Keys.
00:05:16Das ist noch okay, aber treiben wir es auf die Spitze. Wenn wir 199.999 zufällige Keys
00:05:20in den Payload packen und unser Array bei Index Null von „$k1“, „$k2“ bis zu „$k1000“ ändern,
00:05:24muss für jeden dieser 1000 Pointer die Liste der 200.000 Keys durchsucht werden.
00:05:28Das ergibt insgesamt 200 Millionen String-Vergleiche. Wie man sich denken kann,
00:05:32blockiert das den Thread für einige Sekunden. Hier ist der Commit, der das Problem behoben hat.
00:05:36Er ist ziemlich komplex, aber ich versuche es so gut wie möglich zu erklären. Im Grunde
00:05:41nutzen sie jetzt ein Cursor-basiertes System. Alle 200.000 Keys werden in eine Liste geladen,
00:05:44und bei der Suche nach „$k1“ wandert ein Cursor die Liste herab, der nicht mehr zurückgehen kann.
00:05:48Er prüft „$j1“ – kein Match –, dann „$j2“ und so weiter bis ganz nach unten zu „$j199.999“.
00:05:52Wenn dort kein Treffer für „$k1“ gefunden wurde, wird mit „$k2“ fortgefahren. Aber da der Cursor
00:05:56bereits am Ende der Liste steht und nicht zurückgehen kann, ist „$k2“ sofort „undefined“.
00:06:03Das setzt sich bis „$k1000“ fort. Diesmal sind wir also insgesamt nur einmal über die 200.000 Keys gegangen.
00:06:07Dieser Fix reduziert die Operationen von k * n (Anzahl Pointer mal Keys) auf n + k.
00:06:12In unserem Beispiel von 200 Millionen auf 201.000 Operationen. Dieser Tweet von Prime
00:06:17trifft den Nagel auf den Kopf: Ein eigenes Protokoll mit Serialisierung zu bauen, ist extrem schwer.
00:06:21Daher überraschen diese Probleme nicht. Meiner Meinung nach sollten sie Claude Mythos mal
00:06:25über den React- und Next.js-Code schauen lassen. Als Nächstes haben wir den CVE mit der höchsten Schwere:
00:06:28Server-Side Request Forgery (SSRF) in Next.js-Anwendungen. Dieser liegt bei 8,6 von 10.
00:06:33Wichtig: Vercel-Deployments waren nicht betroffen, nur Self-Hosted oder andere Provider.
00:06:36Sie laden also alle diese 200.000 Schlüssel, die wir in unserem Payload gesendet haben, in eine Liste und dann
00:06:41beginnen wir hier bei Null, wo nach der $k1-Referenz gesucht wird, und es geht
00:06:45diese Liste mit einem Cursor nach unten, der nicht zurückgehen kann. Es geht also hier runter zu $j1, sieht, dass das
00:06:50nicht mit dem benötigten Unterstrich übereinstimmt, also geht es zu $j2, was auch nicht
00:06:54mit dem Unterstrich übereinstimmt, also geht es die ganze Liste weiter runter bis zu $j199.999. Sobald es
00:07:01hier angekommen ist, merkt es, dass es keine Übereinstimmung für $k1 gibt, also geht es weiter zu $k2. Nun sucht $k2
00:07:06nach dem zweiten Unterstrich, aber das Problem ist, da dies ein cursorbasiertes System ist und dieser Cursor
00:07:09nicht zurückgehen kann, läuft er sofort am Ende der Liste aus, sodass dies auch
00:07:14nicht definiert sein wird, und das setzt sich bis zu $k1000 fort. Dieses Mal sind wir also nur
00:07:18über 200.000 Schlüssel gegangen. Im Grunde hat dieser Fix die Anzahl der Operationen von
00:07:23$k*n, wobei $k die Anzahl der $k-Referenzen und $n die Anzahl der Schlüssel ist, gesenkt
00:07:27auf $n+k. In unserem Fall gingen wir von 200.000.000 Operationen runter auf 201.000, da
00:07:33es immer noch alle Schlüssel und auch diese $k-Referenzen durchgehen muss. Ich finde,
00:07:37dieser Tweet von Prime fasst die Situation, in der wir uns befinden, wirklich gut zusammen. Ein eigenes Protokoll mit Serialisierung
00:07:41zu erstellen, ist unglaublich schwer, daher überrascht es nicht, dass wir so viele Probleme sehen. Meiner Meinung nach
00:07:46sollten sie Claude Mythos mal über die React- und Next.js-Codebase schauen lassen. Als
00:07:50nächstes haben wir die schwerwiegendste CVE von allen, nämlich Server-Side Request Forgery in
00:07:54Next.js-Anwendungen. Wie man sieht, ist diese mit 8,6 von 10 eingestuft, aber es ist auch
00:07:59erwähnenswert, dass dies keine Vercel-gehosteten Deployments betraf, sondern nur Self-Hosted oder andere Anbieter.
00:08:04Dieser Exploit ist zudem super einfach auszunutzen. Zuerst müssen wir unseren Next.js-Server starten,
00:08:09und auch hier kann es eine Standard-Next.js-Anwendung sein. Man muss keine Änderungen vornehmen.
00:08:14Als Nächstes brauchen wir auch einen internen Server. Nehmen wir an, dieser Server könnte nur vom
00:08:18Next.js-Server und nicht von der Außenwelt erreicht werden. Sagen wir, er befände sich in unserem Cloud-Deployment.
00:08:23Dann senden wir einfach eine sehr simple Curl-Anfrage, wobei wir eine
00:08:26Curl-Anfrage an unsere Next.js-Anwendung schicken. Das war auf Port 3002, und wir sagen, dass unser
00:08:31einzutauchen. Lasst mich wissen, ob ihr noch dabei seid, indem ihr etwas Beliebiges kommentiert,
00:08:36vielleicht „Bar“ oder so, und abonniert, wenn euch der Content gefällt. Zwei Kategorien fehlen noch,
00:08:40aber die gehen schneller. Zuerst Cache Poisoning; hier konnte ich dieses moderate Problem nachstellen.
00:08:45Es handelt sich um Cache Poisoning in React Server Components, Schweregrad 5,4 von 10.
00:08:49Dafür nutze ich eine Next.js-App und ein Fake-CDN, um eine echte Deployment-Umgebung zu simulieren.
00:08:53Wenn ich die Seite zum ersten Mal über die CDN-URL besuche und Produkte anklicke, ist es
00:08:57beim ersten Mal ein Cache-Miss und danach ein Hit. In den Logs sieht man das:
00:09:02Erst ein Miss für „/products“ mit Query-String, dann Hits. Als Nächstes habe ich den Cache geleert,
00:09:06um einen Ablauf zu simulieren, und sende nun diesen Curl-Request. Wenn ich jetzt in der App
00:09:10einen curl-Request an den Next.js-Server gesendet und gesagt: „Hey, kannst du in unserem Namen
00:09:15eine Anfrage an den internen Dienst senden?“ So haben wir diese Informationen erhalten und die Firewall umgangen,
00:09:19gibt die URL Server-Component-Daten statt HTML zurück. Next prüft dann beim Caching,
00:09:23um welche Art von Daten es sich handelt, und speichert sie entsprechend ab. Theoretisch sollte ein Nutzer,
00:09:28der die HTML-Version anfordert, niemals Server-Component-Daten erhalten. Das Problem:
00:09:32Durch unseren Query-String am Ende des Curl-Requests schlug die Prüfung fehl,
00:09:37da diese nur prüft, ob die URL auf „.rsc“ endet. Da unser Link auf den Query-String endet,
00:09:40hielt das System es für HTML und speicherte die Server-Component-Daten fälschlicherweise als HTML im Cache.
00:09:45Der nächste Nutzer, der die Seite aufrief, erhielt also die Rohdaten statt der Seite. Der Fix dafür
00:09:49war denkbar einfach: Bei der Prüfung auf die Endung „.rsc“ werden Query-Strings nun ignoriert.
00:09:55Kommen wir zur letzten Kategorie: Cross-Site Scripting (XSS). Hier habe ich eine Rekonstruktion
00:09:58mit einer Bewertung von 6,1 von 10. Es betrifft XSS in „beforeInteractive“-Skripten
00:10:02mit nicht vertrauenswürdigen Eingaben. Das heißt: Wenn ich in Next.js ein Skript-Tag
00:10:06mit der Strategie „beforeInteractive“ nutze und ein Attribut davon Daten aus einer unsicheren Quelle erhält –
00:10:11wie hier aus den Search-Params –, ist XSS möglich. Ich kann einen Nutzer dazu bringen,
00:10:15auf einen Link wie diesen zu klicken, in dem Schadcode im Search-Param eingebettet ist. Wer darauf klickt,
00:10:20sieht dann zum Beispiel das hier: Man denkt, man müsse sich neu einloggen, aber beim Klick auf „Sign In“
00:10:24sieht man, dass es ein Fake-Formular war, das über den Parameter eingeschleust wurde.
00:10:27Damit kann JavaScript auf dem Rechner des Opfers ausgeführt werden. Ein realistisches Beispiel wäre
00:10:32das Stehlen von Session-Cookies, um sich Zugang zu allen Konten zu verschaffen. Die Ursache
00:10:36Anfrage ist, also 200, 404 oder Ähnliches. Wenn also ein Statuscode vorhanden ist,
00:10:41zuerst das Skript-Tag und öffne dann ein neues mit meinem Code. Wenn ich Enter drücke,
00:10:45erscheint die Meldung „pwned“. Wie gesagt, man braucht ein Skript-Tag mit der Strategie
00:10:49„beforeInteractive“ und ein Attribut aus einer unsicheren Quelle wie den Query-Parametern. In Next.js
00:10:53wird dieses Tag in so etwas wie „dangerouslySetInnerHTML“ umgewandelt – der Name sagt schon alles.
00:10:58Dabei wird „JSON.stringify“ verwendet. Wichtig ist: „JSON.stringify“ maskiert keine
00:11:01HTML-Zeichen wie schließende Klammern. Das Skript sucht nach der Quelle und die restlichen Props
00:11:04enthalten die Tracking-ID sowie unseren manipulierten Wert aus den Query-Parametern.
00:11:09Das wird in den JSON-String gepackt und auf der Seite gerendert. In der finalen HTML-Struktur
00:11:14sieht es dann so aus: Wir haben das Skript-Tag mit der ID, aber dann folgt direkt
00:11:18ein schließendes Skript-Tag, das das ursprüngliche Tag beendet. Dahinter können wir jeden beliebigen
00:11:23Code ausführen. Der Rest am Ende sorgt dafür, dass keine Fehlermeldungen auf der Seite erscheinen,
00:11:27sodass keine offensichtlichen Anzeichen für den Angriff zu sehen sind. Das waren 13 CVEs
00:11:31für Next.js und React in dieser Woche. Ich weiß ehrlich gesagt nicht, was ich davon halten soll.
00:11:35Ich hasse das, und das sage ich als jemand, der vor zwei Jahren jedes Projekt mit Next.js umgesetzt hat
00:11:39und dachte, es sei die Zukunft. Aber es scheint eine Hürde nach der anderen zu geben.
00:11:44Es wirkt, als hätten sie Dinge überstürzt und müssten sie nun im Nachhinein flicken. Ich persönlich
00:11:47bin mittlerweile voll bei TanStack und auch bei Astro, wenn ich eine inhaltsbasierte Seite brauche.
00:11:52Die wirken auf mich viel simpler. Auch was Cloudflare in letzter Zeit macht, gefällt mir sehr,
00:11:58daher ziehe ich meine Projekte langsam dorthin um. Aber ich habe immer noch etwa 20 auf Vercel,
00:12:02die ich jetzt aktualisieren muss. Was denkt ihr? Werden Server Components jemals nützlich sein,
00:12:07oder sind wir damit gescheitert? Schreibt es mir in die Kommentare. Abonniert den Kanal
00:12:11und wir sehen uns wie immer im nächsten Video.
00:12:16---
00:12:20---
00:12:24---
00:12:29---
00:12:33---
00:12:37---
00:12:41---
00:12:46---
00:12:50---
00:12:55---
00:12:59---
00:13:03---
00:13:08---
00:13:12---
00:13:16---
00:13:19---
00:13:22---
00:13:26---
00:13:31---
00:13:34---
00:13:39---
00:13:43---
00:13:47---
00:13:51---
00:13:55---
00:13:59---
00:14:04---
00:14:08---
00:14:12---
00:14:17---
00:14:21---
00:14:24---
00:14:29---
00:14:33---
00:14:37---
00:14:41---
00:14:45---
00:14:49---
00:14:53---
00:14:58---
00:15:01---
00:15:05---
00:15:09---
00:15:14---
00:15:18---
00:15:22---
00:15:26---
00:15:31---
00:15:35---
00:15:39---
00:15:43---
00:15:48---
00:15:51---