Blog / CVE-Erklärungen

fast-jwt-Algorithmenverwechslung: beim dritten Mal behoben

fast-jwt hat RSA-zu-HS256-Algorithmenverwechslung dreimal gepatcht. CVE-2026-107722 zeigt, warum die ersten zwei Fixes eine Eingabe schlossen statt der Regel.

Promotionsstudium in Informationssicherheit abgeschlossenUniversitäre Verteidigung bestanden · Abschließende Dissertationsverteidigung steht noch aus

Derselbe Fehler, dreimal gepatcht

Kein Passwort. Trotzdem Admin-Zugriff. Das bringt ein gefälschtes Token gegen den RS256-Verifizierer von fast-jwt — ein Fehler, der dreimal gepatcht wurde, bevor der Fix tatsächlich hielt. Am 8. Oktober 2026 veröffentlichte die GitHub Advisory Database CVE-2026-107722 — den Bericht hinter dem Fix, der endlich hielt. [1] Lesen Sie den Titel langsam: „Incomplete patch of CVE-2026-34950" — ein unvollständiger Patch für CVE-2026-34950. Und nun der Titel von CVE-2026-34950 selbst, von April: „Incomplete fix for CVE-2023-48223" — eine unvollständige Behebung von CVE-2023-48223. [3]

Drei Advisories, eine Bibliothek, eine Fehlerklasse. Jeder Fix schloss exakt die Eingabe, die der vorherige Forscher demonstriert hatte, und jedes Mal kam jemand mit einer leicht anderen Eingabe und kam glatt durch.

Diese Kette ist die eigentliche Geschichte. Die Schwachstelle selbst ist Lehrbuch-Algorithmenverwechslung, und wenn das alles ist, was Sie mitnehmen, patchen Sie auf 6.3.0 und machen weiter. Lohnenswert ist die Frage, warum zwei kompetente Fixes scheiterten und der dritte nicht — dieser Unterschied lässt sich schon am Montag auf Ihre eigenen Patches anwenden.

Ich habe dies nicht selbst in einem Labor nachgestellt. Alles Folgende stammt aus dem Advisory, dem Advisory davor und dem Diff, der beide behoben hat. [1][2][3]

CVE-2023-48223   the original
      v
CVE-2026-34950   "incomplete fix"   9.1
      v                (added trim)
CVE-2026-107722  "incomplete patch" 9.8

Der Server vertraut dem Schlüssel. Der Angreifer kontrolliert die Zeichenkette

JWT-Algorithmenverwechslung in einem Satz: Das Token sagt dem Server selbst, wie es zu verifizieren ist. Signiert eine Anwendung mit RS256 und lässt sich der Verifizierer stattdessen zu HS256 überreden, hört der öffentliche RSA-Schlüssel auf, ein öffentlicher Schlüssel zu sein, und wird zum gemeinsamen Geheimnis. Er ist öffentlich. Der Angreifer besitzt ihn. Er signiert damit ein Token, der Server berechnet denselben HMAC, und die Signatur stimmt überein.

Interessant an fast-jwt ist, wo genau diese Entscheidung fällt. Die Bibliothek bestimmt, ob Ihr Schlüssel asymmetrisch ist, indem sie ihn als Zeichenkette betrachtet: Sieht er wie ein PEM-Block aus, wird er als RSA behandelt; sieht er nicht so aus, als rohes HMAC-Geheimnis. [1]

Das ist ein vernünftiges Vorgehen, und fast jede JWT-Bibliothek macht etwas Ähnliches. Es bedeutet aber auch, dass die Klassifizierung von Textabgleich abhängt, und Textabgleich hat Grenzfälle, die Kryptografie nicht hat.

Warum trim() wie der richtige Fix aussah

Im April meldete CVE-2026-34950, dass ein führendes Leerzeichen die PEM-Prüfung umgeht. [3] Der Matcher war ein am Zeichenkettenanfang verankerter regulärer Ausdruck, sodass ein mit Leerraum beginnender Schlüssel nicht traf, als rohes Geheimnis klassifiziert wurde und der HMAC-Pfad sich öffnete.

Der Fix in 6.2.0 war ein Aufruf von trim() vor der Prüfung. [1][2] Minimal, für die gemeldete Eingabe offensichtlich korrekt, mit Regressionstest ausgeliefert. Ich vermute, die meisten Reviewer hätten ihn in unter einer Minute freigegeben, und ich wäre vermutlich einer davon gewesen.

Aber die gemeldete Eingabe war nie der eigentliche Fehler. Der Fehler war die Verankerung. Ein regulärer Ausdruck, der mit einem Zirkumflex beginnt, kodiert eine Annahme: Der PEM-Header steht an Position null. trim() entfernt diese Annahme nicht, es macht sie nur für eine Kategorie von Zeichen wahr, weil String.prototype.trim nur entfernt, was die ECMAScript-Spezifikation als Leerraum einstuft. [1]

Alles andere funktioniert weiterhin. Die mit dem eigentlichen Fix ausgelieferten Regressionstests zählen auf, was „alles andere" in der Praxis bedeutet: ein NUL-Byte, ein Escape-Zeichen, DEL, Zeichen mit Nullbreite, eine Kommentarzeile, ein HTTP-Wrapper. [2] Jedes davon vor dem PEM-Header, und der Schlüssel ist wieder ein rohes Geheimnis.

Das Advisory formuliert es besser, als ich es könnte: Der Fix war textuell eng gefasst, nur Leerraum, statt die Klasse als Ganzes zu adressieren. [1]

// 6.1.0 and earlier
/^-----BEGIN(?: (RSA))? PUBLIC KEY-----/

// 6.2.0: trim() first, same anchor
trimmedKey.match(publicKeyPemMatcher)

// a leading NUL still walks past it

Der dritte Fix änderte die Regel, nicht die Eingabe

Sehen Sie sich an, was 6.3.0 tatsächlich anders macht, denn es ist keine längere Liste zu entfernender Zeichen. [2]

Die Verankerung fällt ganz weg. Ein neuer Matcher sucht einen PEM-Header an beliebiger Stelle in der Zeichenkette, und der Schlüssel wird von dort abgeschnitten. Ein Schlüssel gilt nur dann als rohes HMAC-Geheimnis, wenn er überhaupt keinen PEM-Header enthält, und sobald ein Header vorhanden ist, kann der Schlüssel nie mehr auf HMAC zurückfallen. Die X.509-Prüfung wechselt von includes zu startsWith. Ein Schlüssel mit PEM-Markern, der sich nicht eindeutig einordnen lässt, wirft nun einen Fehler, statt still in den HMAC-Zweig durchzufallen.

Die Commit-Nachricht formuliert die neue Regel in einem Satz, und es ist die Art Satz, die man für die eigene Codebasis stehlen sollte: Sobald ein Header vorhanden ist, muss der Schlüssel als asymmetrisches Schlüsselmaterial eingestuft werden und kann nie auf HMAC zurückfallen. [2]

Die ersten beiden Fixes setzten eine Eingabe auf eine Sperrliste. Der dritte setzte eine Form auf eine Erlaubnisliste. Das ist der ganze Unterschied, und deshalb schließt dieser die Klasse, statt nur eine weitere Instanz davon.

// 6.3.0 - find it anywhere
/-----BEGIN [A-Z0-9 ]+?-----/

const { pem, isRawSecret } =
  locatePem(trimmedKey)

if (isRawSecret) return hsAlgorithms
// unless algorithms explicitly pins HS256 - that still skips this check

Den Algorithmus festzulegen hilft. Den falschen festzulegen nicht

Hier liegt die Stelle, an der ich wetten würde, dass Teams hereinfallen. Das Advisory listet die verwundbaren Konfigurationen: keine algorithms-Positivliste, ein asynchroner Schlüssel-Callback, oder algorithms auf HS256 gesetzt. Als geschützte Konfiguration gilt algorithms auf RS256 gesetzt. [1]

Den Algorithmus festzulegen ist der Standardrat gegen Algorithmenverwechslung, und es ist richtiger Rat. Aber ein Team, das ein Advisory überfliegt, HS256 in der Angriffsbeschreibung sieht und auf HS256 festlegt, hat den Verifizierer auf die Wahl des Angreifers festgelegt. Die Positivliste muss den Algorithmus nennen, mit dem tatsächlich signiert wird, nicht den aus der Analyse.

Wenn ich Authentifizierungscode prüfe, ist das die Form, nach der ich zuerst suche: eine Schutzmaßnahme, die nach dem Vorfallbericht konfiguriert wurde statt nach dem eigenen Entwurf des Systems. Sie besteht die Prüfung, weil sie sichtbar eine Schutzmaßnahme ist — und zeigt dabei in die falsche Richtung.

Ist Ihr Dienst tatsächlich betroffen?

Enger gefasst, als 9.8 vermuten lässt, und drei Dinge müssen zusammentreffen. [1]

Erstens: Die tatsächlich aufgelöste Version von fast-jwt liegt zwischen 6.2.0 und 6.2.4 für genau diese CVE — 6.1.0 und früher ist ebenfalls nicht sicher, sondern dem zweiten Glied dieser Kette ausgesetzt, CVE-2026-34950. Prüfen Sie, was npm wirklich installiert hat, nicht was package.json verlangt, und prüfen Sie die transitive Abhängigkeit: fast-jwt ist das Standard-JWT-Backend von @fastify/jwt, sodass etliche Teams es nutzen, ohne es selbst gewählt zu haben. [1]

Zweitens ist der Verifizierer auf eine der verwundbaren Arten oben konfiguriert. Drittens, und das ist das Einzige, das niemand für Sie prüfen kann: Ihr konfigurierter Schlüssel trägt vor seinem PEM-Header ein Zeichen, das kein Leerraum ist. Ein sauberer Schlüssel ist überhaupt nicht betroffen.

Diese letzte Bedingung ist eine Eigenschaft dessen, wie der Schlüssel den Prozess erreicht, nicht Ihres Codes. Aus einer Datei gelesen, in eine Umgebungsvariable eingefügt, aus einem Secrets-Manager gezogen, von einem Deployment-Skript mit einer Beschriftung zusammengefügt. Ich habe keine Daten darüber, wie oft das in freier Wildbahn vorkommt, und das Advisory auch nicht — behandeln Sie es also als Hinweis, wo zu suchen ist, nicht als Wahrscheinlichkeit.

Was heute zu prüfen ist

Drei Dinge, in der Reihenfolge, in der ich sie selbst angehen würde.

Aktualisieren Sie auf 6.3.0 und bestätigen Sie danach die aufgelöste Version. Setzen Sie dann eine explizite algorithms-Positivliste, die den asymmetrischen Algorithmus nennt, den Ihr Dienst tatsächlich verwendet. Und dann, das Einzige, das diese CVE überdauert: Prüfen Sie die Form Ihres Verifizierungsschlüssels beim Start, statt dem zu vertrauen, was Ihnen die Datei oder die Umgebungsvariable gerade liefert.

Diese dritte Prüfung kostet eine Zeile und hätte die letzten zwei dieser CVEs in Ihrem Dienst unausnutzbar gemacht — einschließlich der nächsten. (Die erste, CVE-2023-48223, hatte eine andere Form — ein gültiger Schlüssel, den der alte Matcher nicht erkannte, kein Müllpräfix davor — diese Prüfung hätte sie also nicht erfasst.)

Das ist die eigentliche Lehre. Eine Bibliothek, die dieselbe Klasse dreimal gepatcht hat, kommt wahrscheinlich wieder. Einem Schlüssellader, der alles ablehnt, was nicht exakt ein PEM-Block ist, ist das gleichgültig.

# the version that actually loads
npm ls fast-jwt

# pin the algorithm you really use
{ algorithms: ['RS256'] }

# assert the key's shape at boot
key.startsWith('-----BEGIN')

Patchen Sie die Regel, nicht den Bericht

Jeder dieser Fixes wurde von jemandem Kompetentem erstellt, als Antwort auf einen echten Bericht, mit Test. Das Versagen lag nicht an Sorglosigkeit. Es lag am Umfang: Der Bericht beschrieb eine Eingabe, also adressierte der Fix die Eingabe, und die Regel, die diese Eingabe gefährlich machte, blieb exakt dieselbe.

Die Frage, die ich bei einem Patch-Review absichtlich unbequem stelle: Welcher Klasse von Eingaben ist dieser Bericht ein Beispiel, und schließt der Fix die Klasse? Lautet die Antwort Nein, haben Sie die nächste CVE nicht verhindert, sondern terminiert.

Diese Frage ist das Wesentliche an einem Secure Code Review. Einen Bericht zurückzuverfolgen bis zu der Entscheidung, die ihn möglich machte, und dann zu prüfen, ob der Fix diese Entscheidung verschoben hat oder nur das Symptom, ist Arbeit, die ich regelmäßig an Kundencode leiste — und genau die Art, die kein Scanner je melden wird, weil die gepatchte Versionsnummer korrekt aussieht. Wenn Sie wollen, dass ein Fix mit den Augen eines Angreifers geprüft wird, bevor jemand anderes die Lücke darin findet, ist das das Gespräch, mit dem Sie beginnen sollten.

Quellen

Soll das auch in Ihrem Produkt geprüft werden?

Security Code-Review
Alle Artikel

08Kontakt

Sie bringen etwas mit Nutzerdaten heraus?

Senden Sie mir eine kurze Beschreibung. Ich antworte innerhalb von drei Tagen mit Rückfragen oder einem Angebotsentwurf; den Preis vereinbaren wir vor Beginn schriftlich.