Früher dachte ich, das Schreiben einer eigenen Authentifizierungsschicht sei ein Ehrenzeichen. Wenn man JWTs versteht und ein Passwort hashen kann, wie schwer kann es dann schon sein? Ich habe ein Node-Backend aufgesetzt, Token ausgegeben und das Ganze als erledigt betrachtet. Der Code funktionierte. Die Tests waren erfolgreich. Dann begann ich zu lesen, wie echte Angriffe tatsächlich ablaufen, und der Boden unter mir brach weg. Jedes Kapitel über Injection, Enumeration und Side-Channel-Leaks schickte mich mit einem flauen Gefühl im Magen zurück zu meinem Editor. Mein Auth-System hatte nicht nur Lücken; es hatte vier sperrangelweit offene Türen, die ich selbst eingebaut hatte. Hier ist, was ich gefunden habe und was ich genau geändert habe.

SQL-Injection durch String-Verkettung

Der erste Fehler war der älteste Trick der Welt. Ich habe Benutzereingaben genommen und sie direkt in SQL-Strings eingefügt. In meiner Login-Route habe ich die E-Mail aus dem Request-Body genommen und sie in eine Abfrage wie SELECT * FROM users WHERE email = '${email}' verkettet. Es fühlte sich harmlos an, weil ich das Frontend kontrollierte. Das ist eine gefährliche Annahme. Ein Angreifer benötigt nicht dein Frontend. Ein einziger manipulierter POST-Body könnte diese Login-Prüfung in einen Datendiebstahl oder eine gelöschte Datenbank verwandeln. Sendet man einen Payload wie ' OR '1'='1 oder schlimmer noch, eine Stacked Query, die Tabellen löscht, und der String wird roh ausgeführt, sind deine Daten weg. Ich hatte der Datenbank keine Möglichkeit gegeben, zwischen meinem Code und den Daten des Angreifers zu unterscheiden.

Die Lösung war nicht mehr Eingabevalidierung oder das manuelle Escapen von Strings. Die wahre Lösung waren parametrisierte Abfragen. Ich bin auf node-postgres umgestiegen und habe angefangen, Platzhalter wie $1 zu verwenden. Die Abfrage wird zu einer Vorlage: SELECT * FROM users WHERE email = $1. Der Treiber sendet das SQL und die Werte über getrennte Kanäle. Die Datenbank behandelt die Eingabe strikt als Daten, egal welche Zeichen sie enthält. Diese eine Änderung schließt die gesamte Klasse von Injection-Angriffen. Es ist einfacher zu lesen, leichter zu warten und nimmt einem die Last ab, jedes Mal ein Regex-Magier sein zu müssen, wenn man eine WHERE-Klausel schreibt.

E-Mail-Enumeration durch Fehlermeldungen

Mein zweiter Fehler sah nach guter UX aus. Wenn ein Benutzer die falsche E-Mail eingab, gab ich User not found zurück. Wenn die E-Mail korrekt war, aber das Passwort falsch, gab ich Incorrect password zurück. Es fühlte sich hilfreich an. Es war jedoch auch ein Werkzeug zur Aufklärung für Angreifer. Enumeration-Skripte können deinen Login-Endpunkt mit tausenden E-Mail-Adressen bombardieren. Wenn sich der Response-Body oder der Statuscode ändert, je nachdem, ob das Konto existiert, kann das Skript eine verifizierte Liste deiner Benutzer erstellen. Diese Liste wird zur Grundlage für Credential Stuffing, gezieltes Phishing und weitere Brute-Force-Versuche.

Ich musste akzeptieren, dass Benutzerfreundlichkeit manchmal der Sicherheit weichen muss. Ich habe jeden fehlgeschlagenen Login-Pfad so geändert, dass er exakt denselben String zurückgibt: Invalid credentials. Keine Hinweise. Keine verzweigte Logik in der Fehlermeldung. Egal, ob die E-Mail fehlt, das Passwort falsch ist oder das Konto gesperrt wurde – der Text bleibt identisch. Dies gilt auch für die Registrierung und die Passwort-Reset-Flows; verrate nicht, ob eine Adresse bereits in deinem System vorhanden ist. Eine generische Nachricht beseitigt ein Informationsleck, von dem Angreifer abhängig sind.

Timing-Angriffe beim Passwortvergleich

Fehler Nummer drei war unsichtbar. Ich war