Ik dacht vroeger dat het schrijven van mijn eigen authenticatielaag een ereteken was. Als je JWT's begrijpt en een wachtwoord kunt hashen, hoe moeilijk kan het dan zijn? Ik heb een Node-backend opgezet, tokens uitgegeven en was klaar. De code werkte. De tests slaagden. Toen ik echter begon te lezen over hoe echte aanvallen in de praktijk plaatsvinden, viel de bodem onder mijn voeten weg. Elk hoofdstuk over injectie, enumeratie en side-channel-lekken stuurde me met een knoop in mijn maag terug naar mijn editor. Mijn auth-systeem had niet alleen gaten; het had vier wagenwijd openstaande deuren die ik zelf had geïnstalleerd. Dit is wat ik vond, en precies wat ik heb veranderd.

SQL-injectie via stringconcatenatie

De eerste bug was het oudste trucje uit het boekje. Ik nam gebruikersinvoer en plaatste deze rechtstreeks in SQL-strings. In mijn login-route pakte ik het e-mailadres uit de request body en voegde dit samen in een query zoals SELECT * FROM users WHERE email = '${email}'. Het voelde onschadelijk omdat ik de frontend controleerde. Dat is een gevaarlijke aanname. Een aanvaller heeft je frontend niet nodig. Eén enkele gemanipuleerde POST-body zou die login-check kunnen veranderen in een datalek of een gewiste database. Stuur een payload zoals ' OR '1'='1 of erger, een stacked query die tabellen verwijdert, en als de string onbewerkt wordt uitgevoerd, is je data weg. Ik had de database geen enkele manier gegeven om onderscheid te maken tussen mijn code en de data van de aanvaller.

De oplossing was niet meer inputvalidatie of het handmatig escapen van strings. De echte oplossing waren geparametriseerde queries. Ik stapte over op node-postgres en begon placeholders zoals $1 te gebruiken. De query wordt een template: SELECT * FROM users WHERE email = $1. De driver stuurt de SQL en de waarden via gescheiden kanalen. De database behandelt de invoer strikt als data, ongeacht welke tekens het bevat. Deze ene wijziging sluit de gehele klasse van injectie-aanvallen. Het is eenvoudiger te lezen, makkelijker te onderhouden en het neemt de last weg om elke keer dat je een WHERE-clause schrijft een regex-wizard te moeten zijn.

E-mail-enumeratie via foutmeldingen

Mijn tweede fout leek op goede UX. Wanneer een gebruiker het verkeerde e-mailadres invoerde, gaf ik Gebruiker niet gevonden terug. Wanneer het e-mailadres wel klopte maar het wachtwoord niet, gaf ik Incorrect wachtwoord terug. Het voelde behulpzaam. Het was echter ook een tool voor verkenning voor aanvallers. Enumeratie-scripts kunnen je login-endpoint bestoken met duizenden e-mailadressen. Als de response body of statuscode verandert afhankelijk van de vraag of het account bestaat, kan het script een geverifieerde lijst van je gebruikers opbouwen. Die lijst vormt de basis voor credential stuffing, gerichte phishing en verdere brute-force-pogingen.

Ik moest accepteren dat gebruiksvriendelijkheid soms moet wijken voor beveiliging. Ik heb elk pad voor een mislukte login aangepast om exact dezelfde string terug te geven: Ongeldige inloggegevens. Geen hints. Geen vertakkende logica in de foutrespons. Of het e-mailadres nu ontbreekt, het wachtwoord fout is of het account is geblokkeerd, de tekst blijft identiek. Dit geldt ook voor registratie- en wachtwoordreset-flows; onthul niet of een adres al in je systeem staat. Eén generieke melding elimineert een informatielek waar aanvallers op vertrouwen.

Timing-aanvallen bij wachtwoordvergelijking

Bug drie was onzichtbaar. Ik was