Inleiding
Het is je misschien opgevallen dat we op Tweakers.net de laatste tijd nogal vaak over datalekken schrijven. Recent behandelden we nog beveiligingsproblemen op de websites van het Spoorwegmuseum, een aantal supermarkten, Kluwer en Tivoli. De genoemde beveiligingslekken hebben één ding gemeen: ze zijn allemaal veroorzaakt door sql-injectie.
Sql-injectie is een van de aanvalsmethoden die een hacker kan gebruiken als gebruikersinvoer op een website niet met voldoende zorg wordt behandeld. Ook cross site scripting wordt daardoor veroorzaakt. Deze problemen zijn al jaren bekend, maar toch blijken er keer op keer websites kwetsbaar voor te zijn.
Dat het een groot probleem is, blijkt ook uit de top-10 van beveiligingsrisico's die het Open Web Application Security Project opstelde. Injecties, waaronder die van sql, staan op de eerste plaats.
Veel ontwikkelaars lijken dus niet of niet goed genoeg te weten wat sql-injectie is en hoe het kan worden voorkomen. Vandaar dat we het in dit artikel nog één keer uitleggen: wat zijn deze problemen, hoe worden ze veroorzaakt en hoe kunnen ze worden voorkomen?
We richten ons in dit artikel vooral op de talen PHP en MySQL, hoewel het probleem ook bij andere programmeertalen en databasesoftware speelt. PHP en MySQL treffen we echter het meest aan bij datalekken waarbij sql-injectie een rol speelt.
/i/1309945326.png?f=imagenormal)
Bron: XKCD.
Sql-injecties en xss
Om uit te leggen wat sql-injectie is, is enige kennis van programmeertalen nodig. Programmeertalen maken gebruik van strings - simpel gezegd: een verzameling karakters in een bepaalde volgorde. Strings die veranderd kunnen worden, worden variabelen genoemd.
Veel ontwikkelaars maken gebruik van variabele strings om database-opdrachten samen te stellen. Daardoor kan zo'n opdracht dynamisch worden aangepast, bijvoorbeeld als een gebruiker een bepaalde pagina wil opvragen. Voor een inlogsysteem waarbij gebruikersnaam en wachtwoord moeten worden gecontroleerd, zou je bijvoorbeeld de volgende serie opdrachten kunnen gebruiken:

Een normale gebruiker vult gewoon zijn gebruikersnaam en wachtwoord in om in te loggen. In de eerste twee statements wordt uitgelezen wat deze gebruiker heeft ingevoerd. In de derde opdracht (die hier voor de leesbaarheid op twee regels staat) wordt, aan de hand van de invoer, een string met een sql-opdracht samengesteld. De laatste twee opdrachten voeren deze query uit en controleren of de invoer het gewenste resultaat oplevert.
Een kwaadwillende gebruiker doet het anders. Stel dat die kwaadwillende gebruiker wil inloggen met de username 'Joost', dan hoeft hij in het loginveld enkel Joost' --
in te voeren om MySQL om de tuin te leiden. Dat is een sql-injectie.
Het invoerveld $username
is namelijk bedoeld voor pure data, maar door de apostrof toe te voegen, worden de twee koppeltekens door MySQL geïnterpreteerd als een deel van de opdracht, om precies te zijn: "dit is het einde van de opdracht; negeer de rest van de regel." Het gedeelte van de opdracht waarin wordt gecontroleerd of het wachtwoord bij deze user correct is opgegeven, wordt zo overgeslagen. Als we aannemen dat er maar één username 'Joost' is - en dat wordt vrijwel altijd door de database afgedwongen - bevestigt de laatste opdracht uit het codeblok altijd dat naam en wachtwoord bij elkaar horen. Het wachtwoord is dan echter niet daadwerkelijk gecontroleerd.
Er kunnen natuurlijk ook andere opdrachten aan de query worden toegevoegd. Via sql-injectie kunnen zo complete databases worden geplunderd of gemanipuleerd. Dat is geen theoretisch probleem: het gebeurt dagelijks. Het misbruiken van sql-injectie is simpeler dan op het eerste gezicht lijkt, want er zijn kant-en-klare tools die het zware werk doen.

Bij die tools zijn het invoeren van een url en een paar drukken op de knop voldoende: de software gaat dan op zoek naar kwetsbaarheden en, indien aanwezig, slurpt deze desgewenst de hele database leeg. De effectiviteit verschilt van geval tot geval, maar over het algemeen geldt: als een website lek is, is het gebruiken van deze software kinderlijk eenvoudig.
Cross site scripting
Cross site scripting - vaak afgekort tot xss - is een vergelijkbaar probleem, dat opduikt wanneer user-input wordt verwerkt in de html-output. Een simpel voorbeeld is bij slecht gebouwde zoekmachines te zien. Als een gebruiker daar <script>alert('hoi');</script>
als zoekterm invoert, wordt er niet op die term gezocht, maar wordt het javascript uitgevoerd: er verschijnt een pop-up.
Een kwaadwillende kan een url zo opstellen dat een argeloze gebruiker die erop klikt, ongewild een script laat uitvoeren. Dat script heeft dan toegang tot alle gegevens waar die gebruiker toegang toe heeft. Zo kunnen bijvoorbeeld cookies worden gestolen. Soms kunnen ook variabelen worden gebruikt die aan andere gebruikers worden getoond. Als een forum bijvoorbeeld niet goed is beveiligd, kan een kwaadwillende een script in een posting opnemen, maar ook bijvoorbeeld in zijn username.
Xss kan ook worden misbruikt om beveiligingsproblemen in browsers te misbruiken, bijvoorbeeld door een Flash- of javascript-kwetsbaarheid te injecteren. Vooral input die aan andere gebruikers wordt getoond is daarvoor gevoelig.
Vertrouw nooit een gebruiker
"Het probleem is dat het al bij de start misgaat", zei HP-beveiligingsonderzoeker Alexander Hoole op de RSA Conference, een beveiligingsconferentie die eind februari in San Francisco van start ging. Hoewel sql-injectie vrij eenvoudig te voorkomen is, ontbreekt bij programmeurs toch vaak de benodigde kennis: "In leerboeken staat het al fout."
mysql_real_escape_string
De belangrijkste les is: wantrouw alle data die van de gebruiker afkomstig is. Het beste is om altijd aan te nemen dat een gebruiker te kwader trouw is - de kans dat een van de bezoekers dat ook daadwerkelijk ís, is gezien het grote aantal datalekken redelijk groot.
Om te voorkomen dat MySQL-opdrachten in een variabele worden verstopt, kan in PHP de functie mysql_real_escape_string
worden gebruikt. Die zorgt ervoor dat bepaalde karakters, waarmee het onderscheid tussen data en MySQL-code worden gemaakt, onschadelijk worden gemaakt. Dit noemt men 'escaping': voor een teken als een apostrof wordt dan een backslash geplaatst, zodat dat teken door MySQL als data wordt gezien, en niet als deel van de query.

Deze opdracht zou voldoende moeten zijn om sql-injecties af te vangen. Het is van belang mysql_real_escape_string
op het allerlaatste moment toe te passen: zo wordt voorkomen dat andere, latere bewerkingen nog onverwachte en mogelijk schadelijke resultaten opleveren.
htmlspecialchars
Een vergelijkbare functie is er in php voor het bewerken van user-input voordat deze in html wordt getoond: met htmlspecialchars
worden karakters als '<', '>' en aanhalingstekens vervangen door html-entiteiten, zodat ze door de browser niet als onderdeel van de html-code worden verwerkt. Dit geldt voor alle input die van gebruikers afkomt en niet alleen voor invoervelden die zichtbaar zijn voor de gebruiker. Xss-scripts zijn hiermee effectief tegen te gaan.
Maar uiteindelijk is bescherming tegen kwaadaardige code, hoe noodzakelijk ook, niet genoeg. De website Just-Eat.nl, waar mensen eten kunnen laten thuisbezorgen, liet het bedrag dat mensen moesten betalen door de browser van de gebruiker uitrekenen. Vervolgens werd bedrag dat naar de server gestuurd in een verborgen html-invoer-veld. 'Onzichtbaar' is echter niet hetzelfde als 'niet aan te passen': gebruikers konden het bedrag zelf veranderen en hoefden bijvoorbeeld maar een paar cent voor een complete maaltijd te betalen. Just-Eat.nl vertrouwde erop dat de data van de gebruiker correct was - en dat is dus altijd fout.
Voorbereiding
Sql-query's kunnen ook eerst aan de server worden aangeboden om later pas van variabelen te voorzien; dat worden 'prepared statements' genoemd. Hiermee zorg je er voor dat de server precies weet welke variabelen op welke plek moeten staan, waarna de server op die plaatsen zelf de goede escaping en transformaties kan toepassen. Daarnaast zorgt dit bij herhaaldelijk gebruik van dezelfde query's voor prestatiewinst, omdat een deel van het werk van de databaseserver niet opnieuw hoeft te gebeuren.
In het geval van ons loginscript zou de sql-query er dan zo uitzien:

Ook kun je de user-input onderwerpen aan bepaalde criteria. Als van een gebruiker een numerieke waarde wordt verwacht, bijvoorbeeld een id, controleer dan of de variabele inderdaad een getal is dat aan de eisen voldoet. Zo nee, dan is er waarschijnlijk wat aan de hand. Hiervoor kunnen bijvoorbeeld regular expressions worden gebruikt. Daarbij is whitelisting effectiever dan blacklisting: sta dus alleen bepaalde waarden toe, in plaats van dat je bepaalde waardes uitsluit.
Je eigen code testen kan ook veel problemen voorkomen. Gebruik bijvoorbeeld sqlmap om kwetsbaarheden te ontdekken. Om sql-injectie te voorkomen kan verder object-relational mapping worden gebruikt; onder php is dat bijvoorbeeld mogelijk met het Doctrine-framework. Ook andere ontwikkelframeworks bieden soms ingebouwde bescherming tegen sql-injectie, zoals bijvoorbeeld CodeIgniter. Voor Java is er het Hibernate-framework tegen sql-injectie en Apache Wicket tegen cross site scripting.
Het gebruik van mysql_real_escape_string, prepared statements en datatyping zorgt er alleen voor dat een gebruiker geen sql-query's kan injecteren; ze controleren niet of een ogenschijnlijk legitiem verzoek wel mag worden uitgevoerd. Als een gebruiker toegang heeft tot pagina.php?id=1, wil dat nog niet zeggen of hij ook pagina.php?id=2 zou moeten kunnen bezoeken. Zoals gezegd: er moet niet alleen op de technische geldigheid van een query worden gelet, maar ook op de inhoudelijke geldigheid.
Security through obscurity
Wat sowieso níet aan te raden is, is security through obscurity. Als je website bepaalde functionaliteit ondersteunt, is die functionaliteit per definitie kwetsbaar en moet je erop anticiperen dat deze uiteindelijk aangevallen zal worden.
Zo is het van belang om de database-account die voor een bepaalde transactie wordt gebruikt, niet te veel rechten toe te kennen. Voor het uitlezen van een nieuwsbericht is geen root-toegang tot de gehele database nodig, maar voldoet read-only toegang tot een deel van de database. Mocht er dan alsnog ergens een lek in de website zitten, dan zijn de gevolgen in elk geval beperkt.
Ook is het aan te raden om informatie alleen in een vanaf het web toegankelijke databaseserver op te slaan als dat strikt noodzakelijk is. Webwereld schreef enige tijd geleden over een vliegschool die zijn complete klantenadministratie op een webserver bijhield, waardoor talloze persoonsgegevens - van naw-gegevens tot BKR-noteringen - toegankelijk waren. Er is echter nauwelijks een reden te bedenken om zulke gegevens op een webserver op te slaan.
Tot slot
Uiteraard zijn er nog meer beveiligingproblemen waarbij input van anderen een rol speelt en waarbij input eveneens gevalideerd moeten worden. Zo is het uitwisselen van gegevens via bijvoorbeeld jsonp of xml-feeds een beveiligingsrisico: je moet jezelf er grondig van verzekeren dat de servers waarmee je communiceert, wel veilig zijn.
Ook zijn in dit artikel geen methoden behandeld om sql-injectie en cross site scripting in andere programmeertalen tegen te gaan. In andere talen spelen dezelfde problemen, maar zijn de gevolgen en de oplossingen soms heel verschillend. Zo is sql-injectie in ASP.NET wel een bestaand probleem, maar zijn er andere manieren om het af te vangen, bijvoorbeeld door het gebruik van Linq.
Voor de gemiddelde programmeur die echter even een PHP/MySQL-website in elkaar wil flansen - en daar zijn er nogal wat van - mag de les duidelijk zijn: wantrouw de gegevens die van de gebruiker afkomstig zijn, wees selectief met wat je in je database opslaat en hou rekening met het ergste. Een paranoïde programmeur heeft bijna altijd gelijk.