Het is weer tijd voor een echt technisch artikel, dit keer bedoeld voor mensen die zelf scripts en applicaties schrijven in PHP. Vandaag ga ik het hebben over gevaren die je als PHP-programmeur kunt verwachten: veelvoorkomende fouten in PHP-code die kwaadwillenden kunnen misbruiken.
PHP staat bekend als een taal is die relatief vriendelijk is voor beginners. Helaas komt bij veilig programmeren in PHP, en het veilig schrijven van veilige webapplicaties in het algemeen, heel wat kijken. Vandaar dat dit artikel is opgedeeld in twee delen. Om maar meteen te beginnen, de SQL-injectie.
SQL-injectie
SQL-injectie is een techniek die vaak gebruikt wordt tegen kleinere sites. Zelfgebouwde reactiesystemen, gastenboeken en inlogformulieren zijn net zo vaak, of zelfs vaker, doelwit als grote banken en andere instellingen. De techniek draait om het invoegen (injecteren) van commando’s op plekken waar dit niet wordt verwacht. In velden waar je normaal je naam of bericht zou invoeren, vullen kwaadwillenden delen van SQL-queries in, in de hoop dat het systeem aan de andere kant van het formulier zo ingesteld is dat het de queries ‘blind’ gaat uitvoeren. Dat komt helaas vaker voor dan dat je zou denken.
SQL-injectie is misschien het beste te illustreren met een voorbeeld. Stel je eens voor dat een zuivelmerk een toetje op de markt zou brengen met de wat aparte, maar wel heel catchy naam “Yoghurt en een Gouden Ferrari.” Eenmaal in de supermarkt aangekomen bedenk je dat je wel zin hebt in wat nieuws na het eten, en vraagt dus:
Ik wil graag een pak Yoghurt en een Gouden Ferrari.
De meeste supermarktmedewerkers zouden een pak van die yoghurt-met-de-rare-naam van de schappen halen, of misschien een gewoon pak yoghurt. Misschien zouden ze zelfs hun hoofd schudden bij het verzoek – gouden Ferrari’s? In een buurtsuper? Gezond verstand zorgt er in ieder geval voor dat je niet per ongeluk met een sportwagen thuiskomt.
Computerprogramma’s hebben natuurlijk geen gezond verstand. Databases maken geen onderscheid tussen een heel normaal verzoek, zoals een query die het lidnummer van Fred opvraagt…
SELECT lidnr FROM leden WHERE naam='Fred'
…en deze query, die helemaal geen lidnummer maar de wachtwoorden van alle gebruikers opvraagt:
SELECT lidnr FROM leden WHERE naam='Fred' AND 1=0 UNION ALL
SELECT Password FROM mysql.user WHERE Password != ""; -- '
Kortom, SQL-injectie is dus het plaatsen van commando’s op plekken waar eigenlijk alleen data hoort. Door de invoer goed te kiezen is vaak veel schade toe te richten. Zelfs grote sites als Yahoo, The Pirate Bay en Sony hebben inbraken gehad waarbij de techniek gebruikt werd – en dat terwijl je script verdedigen tegen SQL-injectie in principe niet ingewikkeld is.
String escaping
Een veelgebruikte manier om SQL-injectie tegen te gaan is string escaping. Escaping is het vervangen van tekens met een speciale betekenis in een programmeertaal (aanhalingstekens en dergelijke) door andere die geen speciale betekenis hebben, vaak door er een backslash voor te zetten. Aanhalingstekens worden \”, witregels worden \n, backslashes worden \\ enzovoort.
In PHP wordt string escaping voor MySQL-queries gedaan met de functies mysql_real_escape_string
en mysqli_real_escape_string
, afhankelijk van of je mysql of mysqli gebruikt. Beide functies accepteren één argument, namelijk de string die je wilt escapen, en geven de nieuwe string terug:
echo mysql_real_escape_string("Fred'; DROP leden -- ");
// Fred\'; DROP leden --
String escaping is een goede oplossing voor het vermijden van SQL-injecties, mits je het consequent toepast. Dat consequent toepassen is helaas het lastige gedeelte. Alle informatie die op wat voor manier dan ook door de bezoeker van je site te beinvloeden is – formulieren, cookies – moet precies één keer door de mangel gehaald worden. Meer dan één keer, en je krijgt dubbele backslashes in je database. Minder dan één keer, en je loopt kans op injectie.
Magic quotes
Met de hand strings escapen is kortom niet ideaal, en sowieso geldt dat wie niet bekend is met SQL-injectie waarschijnlijk ook niet op de hoogte is van de noodzaak om invoer op te schonen. De makers van PHP kwamen daarom in versie 2 van de taal met een nieuwe functie: magic quotes. In versie 3 werd de functie standaard aangezet.
De magic quotes-functie vervangt volautomatisch alle aanhalingstekens en backslashes in de invoer van je PHP-script, dus de GET-, POST- en COOKIE-arrays. Je zou bijna kunnen zeggen dat het ‘automagisch’ gebeurt: PHP-programmeurs hoeven het niet expliciet aan te zetten, en het gebeurt volledig transparant. Geen gedoe meer met escape-functies aanroepen – ideaal, zou je zeggen.
Al snel bleek dat zomaar alle invoer van alle scripts escapen niet zonder meer een goede aanpak was. Het grootste probleem was dat ook data die helemaal niet door de mangel hoefde toch ge-escaped werd. Wie de invoer niet naar een database maar naar een bestand schreef, kreeg ineens extra backslashes. Wie dat niet wilde moest óf zelf de magic quotes-functie uitzetten óf de operatie terugdraaien.
Magic quotes brengt nog meer problemen met zich mee. De regels die magic quotes gebruikt om speciale tekens te vervangen is te eenvoudig. Niet alle systemen gebruiken dezelfde tekens voor dezelfde dingen, en dus moet magic quotes een compromis maken tussen te veel escapen (en de gebruiker met backslashes opzadelen waar nooit naar gevraagd is) en te weinig escapen (en de gebruiker vatbaar maken voor injectie).
Tegen de tijd dat duidelijk werd dat niemand echt op magic quotes zat te wachten was het al te laat. Inmiddels waren er al veel PHP-scripts geschreven die afhankelijk waren van de magic quotes – ze uitzetten zou betekenen dat scripts die eerst veilig waren ineens weer vatbaar zouden worden voor SQL-injectie. Daarom bleef magic quotes standaard aan staan in alle volgende versies van PHP, ook al was niemand er blij mee. Met de notitie dat gebruikers er goed aan zouden doen de functie zelf uit te zetten.
In de versie van PHP waar nu aan gewerkt wordt, PHP 6, zijn Magic Quotes eindelijk verdwenen.
Prepared statements
Eraan denken om overal met de hand escaping te doen is een bijna onmogelijke taak. Magic quotes aanzetten, zo hebben we gezien, is ook niet een goede oplossing. Maar wat dan wel?
De beste oplossing om veilig queries te kunnen doen zou zijn om de SQL-code en de invoer van elkaar te scheiden, zodat de invoer onmogelijk als SQL gezien kan worden. Dat is precies wat prepared statements zijn.
Prepared statements zijn SQL-queries met ‘gaten’ er in, of placeholders, die later opgevuld worden met extra invoer. Die extra invoer wordt per definitie niet door de database uitgevoerd, en het prepared statement bevat per definitie geen ‘vuile’ data (invoer, dus).
Queries met prepared statements gaat anders dan zonder. De eerste stap is uiteraard het voorbereiden (preparen) van de query, inclusief placeholders. Daarna worden de gaten gevuld door ze vast te maken (binden) aan variabelen in je programma. Als laatste wordt de prepared statement uitgevoerd en het resultaat opgehaald.
<?php
$db = new mysqli("localhost", "gebruikersnaam", "wachtwoord", "database");
$naam = "Fred";
// Zonder escaping: onveilig!
$result = $db->query("SELECT * FROM leden WHERE naam='$naam'");
echo $result->fetch_object()->naam;
// Zonder prepared statements, maar met escaping
$result = $db->query('SELECT * FROM leden WHERE naam="'.$db->escape_string($naam).'"');
echo $result->fetch_object()->naam;
// Zelfde query, maar dan met prepared statements
$stmt = $db->prepare('SELECT * FROM leden WHERE naam=?');
$stmt->bind_param("s", $naam);
$stmt->execute();
$result = $stmt->get_result();
echo $result->fetch_object()->naam;
Prepared statements zijn niet alléén handig omdat ze SQL-injectie tegengaan, ze zijn ook nog sneller, met name als je dezelfde query meerdere keren gebruikt. In plaats van steeds opnieuw een hele query naar de database te sturen, hoef je maar één keer de query voor te bereiden en vervolgens alleen maar de data mee te geven.
Object Relational Mappers
Helaas leidt het gebruiken van prepared queries wel tot iets meer typewerk dan ‘gewone’ MySQL-queries. Als je dan toch bezig bent om de database-integratie van je PHP-applicatie helemaal van deze tijd te maken, is het ook een idee om te kijken naar Object-Relational Mappers.
ORM-systemen vormen een laag tussen de database en je programma. Ze laten je met de tabellen in je database werken alsof het PHP-objecten zijn, en hebben functies die het onnodig maken om met de hand SQL-queries te schrijven. In feite merk je bijna niet eens meer dat je objecten in een database worden opgeslagen: dat handelt het laagje allemaal af. Neem het voorbeeld hierboven, maar dan met het ORM-systeem Propel:
<?php
$fred = LidQuery::create()->filterByNaam('Fred')->find();
echo $fred->getNaam();
Naast hun hoofdfunctie, het omzetten (mappen) van rijen (relations) naar PHP-objecten, zijn ORM-systemen ook handig omdat ze tegen SQL-injectie beschermen. De code in het het ORM-systeem die met de database ‘praat’ gebruikt over het algemeen netjes prepared statements, zodat jij daar niet over na hoeft te denken.
Bekende ORM-systemen voor PHP zijn onder meer het genoemde Propel en Doctrine. Beide zijn vrij grote, complexe systemen, met een bijbehorende leercurve, met name geschikt voor grote applicaties met veel tabellen. Toch is het de moeite waard om eens naar ORM’s te kijken.
Volgende week
In deel 2 van dit artikel komen cross-site scripting (XSS) en cross-site request forgery (CSRF) aan bod. In tegenstelling tot SQL-injectie, die gericht is tegen de server, proberen aanvallers met deze technieken browsers van andere bezoekers van je applicatie te misbruiken voor hun eigen kwaadaardige plannen. Meer daarover dus volgende week.
Nu al meer weten? Eind juni schreef Ralph over het veilig opslaan van wachtwoorden in je eigen applicaties. Zeker het lezen waard!