Bijna elke applicatie die we tegenwoordig maken, vereist authenticatie en autorisatie. Ondanks frameworks, zoals Spring Security, moet je als developer zelf aan de slag om functionaliteit te bouwen. Denk aan accounts aanmaken, wachtwoord herstel, profielpagina, etc. Daarnaast moet je ook nog nadenken over onderliggende technische zaken, zoals wachtwoord opslag, beveiligingsmaatregelen, etc. Dit artikel helpt je om hierbij meer inzicht te krijgen in de veiligheidsaspecten.
Auteur: Bram Patelski
Laten we beginnen met de grote boze wolf: SQL injection. Een login-formulier is een veelgebruikte plek om SQL injection aanvallen uit te proberen. Recentelijk gaf ik een gastcollege en liet daar onderstaande XKCD comic van Bobby Tables zien. Iemand vroeg toen of SQL injection niet allang verleden tijd was. Ik liet hem de site https://bobby-tables.com zien. Deze site is speciaal voor ontwikkelaars opgezet om te helpen SQL Injection te voorkomen. Het bevat codevoorbeelden voor elke denkbare moderne programmeertaal. De site bestaat bijna 10 jaar, maar wordt nog steeds up-to-date gehouden.
Injection is nog steeds de meest misbruikte kwetsbaarheid in webapplicaties, volgens de OWASP Top 10. Hieronder vallen ook andere injection attacks, zoals XPath, LDAP, XML en command injection. Specifiek SQL injections staan aan de basis van meer dan de helft van alle aanvallen. Daarmee is het een belangrijke oorzaak van het uitlekken van databases. Zorg er dus voor dat je gebruik maakt van de maatregelen die de taal en frameworks je bieden. Je hebt hierbij de keuze uit bijvoorbeeld: PreparedStatement, geparameteriseerde queries, maar ook ORM frameworks, zoals Hibernate of Spring Data JPA.
Dit is wat we allemaal wel kennen: minimale lengte en minstens een cijfer, een hoofdletter, een kleine letter en een symbool. Wat we vaak niet beseffen, is dat een wachtwoord, zoals ”P@ssw0rd”, door veel systemen als een veilig wachtwoord wordt beschouwd. Dit terwijl het meer dan 50.000 keer gemeld is bij Troy Hunt’s Pwnd Password API. Niet alleen werken deze regels het bedenken van olifantenpaden in de hand, maar het beperkt ook bij het gebruik van wachtwoord-zinnen. Dit terwijl een wachtwoord-zin van 4 willekeurige woorden maar liefst 7000 tot 4 miljoen keer moeilijker te kraken is dan een wachtwoord van 8 alfanumerieke tekens. Voor details hierover en meer statistieken, zie de scripts in bron Security Examples.
De gangbare complexiteitsregels werken dus niet. Maar simpelweg loslaten van deze regels leidt gegarandeerd tot veel onveilige wachtwoorden. Laten we kijken welke hulpmiddelen je kunt inzetten om dit te voorkomen.
Dit is het meest controversiële hulpmiddel op deze lijst. De meest simpele meters falen al bij ”P@ssw0rd” en de meeste meters zullen ook falen in het herkennen van bekende patronen of de 10.000 meest gebruikte wachtwoorden. Op dit laatste punt komen we later terug. Uitzondering op de regel is ZXCVBN. Deze library analyseert wachtwoorden op dezelfde manier als wachtwoordkrakers dit doen, namelijk door het herkennen van patronen, namen, Engelse woorden, datums en (de meeste) uitgelekte wachtwoorden. Let op: een complexiteitsmeter zet je in als hint en dwingt dus niets af. De gebruiker kan nog altijd een zwak wachtwoord kiezen.
Wel af te dwingen, maar lastiger uit te leggen, is een controle op uitgelekte wachtwoorden. Hierbij blokkeer je wachtwoorden die bekend zijn in uitgelekte wachtwoordlijsten. De Pwned Password API helpt je hierbij door op een veilige manier te controleren of een wachtwoord op dergelijke lijsten staat. Je kunt deze controle doen bij het instellen of veranderen van een wachtwoord. Daarnaast kan je het ook als basis voor advies gebruiken bij het inloggen. Door de controle via SHA-1 hashes te doen en de rang-API te gebruiken, kan je tevens voorkomen dat potentiële wachtwoorden uitlekken voordat ze zijn gebruikt.
Hash het wachtwoord client-side (of in de applicatie) en vraag alleen de eerste 5 tekens van deze hash uit via de range-API. Deze antwoord dan met alle uitgelekte wachtwoorden (in hash) die met deze 5 tekens beginnen. Hierbij worden de eerste 5 tekens weggelaten, aangezien deze toch al bekend zijn. Controleer vervolgens weer client-side of de hash in deze lijst voorkomt om te bepalen of het wachtwoord veilig is.
In compacte vorm is deze code slechts 3 regels lang. In onderstaande Groovy code wordt regel voor regel het volgende uitgevoerd:
Het belangrijkste wat je kunt doen, is gebruikers helpen bij het maken van de juiste keuzes. Adviseer lange wachtwoorden en maak multi-factor authenticatie eenvoudig. Leg uit waarom je bepaalde wachtwoorden blokkeert en adviseer (en omarm) het gebruik van wachtwoordmanagers.
Helaas komt het nog regelmatig voor dat via een datalek leesbare wachtwoorden bekend worden. Er zijn verschillende maatregelen die je kunt treffen om bij een datalek te voorkomen dat de wachtwoorden van je gebruikers uitlekken.
Ondanks bekende zwakheden in SHA-1 en MD5 is het niet mogelijk om bestaande hashes terug te rekenen naar het oorspronkelijke wachtwoord. Dit komt, omdat de hash een digest van het oorspronkelijke wachtwoord is. Vergelijk het met dit voorbeeld: 10 MOD 3 = 1 staat tot <password> MOD <hash-method> = <hash>. Op basis van de formule X MOD 3 = 1 kun je niet uitrekenen wat de waarde van X is. In dat geval kan X 10 zijn, maar ook 4, 7 of 13. Kortom: een hash-functie is niet omkeerbaar. Het gaat alleen heen, maar niet terug.
Een aanval op gehashte wachtwoorden bestaat dan ook veelal uit het opzoeken van hashes in rainbow-tables. Simpel gezegd, zijn dit tabellen met hashes van uitgelekte of veel voorkomende wachtwoorden. Omdat de hash van een bepaald wachtwoord met hetzelfde hashing algoritme altijd hetzelfde is, kan een aanvaller simpelweg bekende wachtwoorden zelf hashen en vergelijken met de hashes die zijn uitgelekt. Dergelijke tabellen zijn vrij eenvoudig op internet te vinden en er bestaan zelfs sites waar je hashes kunt opzoeken.
Een uitstekende verdediging hiertegen is het gebruik van salt. Hierbij genereer je bij een nieuw account een willekeurige string die je opslaat bij het nieuwe account. Je combineert deze salt samen met het wachtwoord om een hash aan te maken. Deze laatste sla je ook op. Bij een login neem je de opgeslagen salt mee. Omdat de salt voor elk account anders is, zullen deze accounts zelfs bij een identiek wachtwoord een andere hash krijgen. Deze methode wordt dan ook wel ‘dynamic salt’ genoemd.
Het nadeel is natuurlijk dat bij een datalek ook alle salts zullen uitlekken. De inzet van salt werkt dan enkel als een vertragende factor. De inzet van pepper geeft een extra beveiligingslaag aan de opslag. We breiden het algoritme uit met een externe sleutel (de pepper) die in een kluis wordt opgeslagen, buiten de database. Deze pepper is voor alle accounts identiek en wordt daarom ook wel ‘static salt’ genoemd. Ook hier gebruiken we de pepper bij zowel het aanmaken van het account als bij de controle van een login.
Om nog een laatste verdediging tegen brute-force aanvallen op te gooien, kunnen we een relatief langzame hash-methode gebruiken. Bekende algoritmen, zoals MD5, SHA-1 en SHA-2, zijn met name geschikt om snel grote hoeveelheden data te hashen. Je komt ze vaak tegen om de integriteit van bestanden te controleren, bijvoorbeeld als je updates of installatiebestanden download. Algoritmen, zoals SCRYPT en BCRYPT, zijn juist opzettelijk (relatief) traag gemaakt, zodat brute-force aanvallen extreem veel tijd kosten. Hieronder is een overzicht van tijden om wachtwoorden in verschillende hash-algoritmen te kraken. Bij SCRYPT en BCRYPT loopt de tijd flink op.
Voor details over deze statistieken en voor code-voorbeelden met Spring Security BCrypt verwijs ik naar de bronvermeldingen.
Ook hier zit weer een nadeel aan. Door bewust een dure operatie in de logica te bouwen, wordt deze vatbaar voor denial-of-service aanvallen. Een aanvaller kan veelvuldig het login-proces aanroepen om de server te overbelasten. Om dit te voorkomen, kan je een maximaal aantal login-pogingen instellen. Hierna kan je een steeds verder oplopende vertraging invoeren voor de aanvaller waarin hij geen log-ins mag uitvoeren. Uiteindelijk kan je de aanvaller zelfs volledig blokkeren van je applicatie. Denk dan wel na over eventueel misbruik van deze beveiliging voor DoS aanvallen.
Wellicht maak je al gebruik van geaggregeerde logging in Splunk of Elastic-stack. Het is vrij eenvoudig om beveiligingsincidenten te monitoren. Houd bijvoorbeeld eens het aantal mislukte login-pogingen bij, of het aantal nieuwe accounts dat wordt aangemaakt in een korte tijdspanne. Zodra dit afwijkt van het gangbare gedrag, dan is het verstandig om te kijken of er iets verdachts aan de hand is. Een SIEM systeem (Security Information and Event Monitoring) concentreert zich op dit soort gedrag en kan je helpen om snel en adequaat aanvallen te detecteren.
Naast de genoemde maatregelen kan je denken aan multifactor authenticatie, inzet van identity-providers, maar denk ook aan risico’s, zoals account-enumeration, wachtwoord hints en beveiligingsvragen.
Login-functionaliteit klinkt vrij simpel en saai. Het is echter wel één van de belangrijkste delen van je applicatiebeveiliging. Het is dan ook essentieel om hier goed over na te denken en de nodige aandacht aan te besteden. Met dit artikel heb ik je inzicht willen geven in verschillende technische maatregelen die je kunt treffen om je applicatie voor gebruikers veiliger te maken en voor aanvallers juist moeilijker.