Prepared statements met PHP en MySQLi

Hoe kan ik mijn website beveiligen tegen SQL-injection aanvallen?“. Het is belangrijk om je website te beveiligen tegen SQL-injection aanvallen. Door middel van SQL-injection kan een aanvaller namelijk inbreken op jouw website, door het “injecteren” van SQL-commando’s, account- en privégegevens stelen, enz. Erg vervelend en onwenselijk natuurlijk.

SQL-injection beveiligingsproblemen

Naar aanleiding van het artikel over het overstappen van ext/mysql naar MySQLi in PHP, ben je vast daarmee bezig. Waarom beveilig je dan niet gelijk jouw website tegen SQL-injection?… SQL-injection is nog steeds een groot probleem.

Met SQL injection kan een aanvaller zelfs een hele databaseserver ontoegankelijk maken door het injecteren van z.g MySQL sleep()-commando‘s, waardoor een MySQL-server wordt opgedragen te blijven wachten.

Wanneer is SQL-injection mogelijk?

Over het algemeen is SQL-injection mogelijk als invoer vanuit de adresbalk (z.g GET-variabelen) niet, of niet goed genoeg, gevalideerd wordt. Als die invoer direct in een query wordt gebruikt, dan is er vaak een probleem.

Een veelgebruikt inlog-voorbeeld is:

$gebruikersnaam = $_GET["gebruikersnaam"];
$wachtwoord = $_GET["wachtwoord"];
$query = "SELECT * FROM `Login` WHERE gebruikersnaam = '$gebruikersnaam' AND wachtwoord = '$wachtwoord'";
$result = mysql_query( $query );

Een bezoeker vult zijn gebruikersnaam en wachtwoord en, en die worden in de database opgezocht. Is er een gebruikersnaam- en wachtwoord-combinatie gevonden, dan is het goed en wordt de bezoeker ingelogd. Lijkt niets mis mee toch? Fout!

Let op: hetzelfde geldt voor niet goed gevalideerde invoer via POST-, REQUEST- en COOKIE-variabelen!

In het bovenstaande PHP-voorbeeld kan een aanvaller extra SQL-commando’s invoeren omdat de invoer niet gefilterd wordt. Een aanvaller kan de uitkomst van de query altijd laten uitkomen op “TRUE”, door het invoeren van een ' teken en daardoor de query te onderbreken. Stel een aanvaller voert als gebruikersnaam admin in, en als wachtwoord: abracadabra' OR 'x'='x. De query wordt hiermee:

SELECT * FROM `Login` WHERE gebruikersnaam='admin' AND wachtwoord='abracadabra' OR 'x'='x'

Het resultaat van deze query is altijd waar (TRUE), omdat 'x' gelijk staat aan 'x'. Ja, het wachtwoord wordt meegegeven in de query, maar heeft hierin geen waarde. Je kunt de query namelijk vertalen naar: Selecteer alles in de tabel “Login”, waar de gebruikersnaam “admin” en het wachtwoord “abracadabra” is OF waar “x” gelijk staat aan “x”.

Omdat 'x'='x' verzekert waar te zijn (TRUE), is de uitkomst van de query TRUE en is de aanvaller ingelogd met een foutief wachtwoord. Zo simpel.

Ook interessant voor jou:  Referer spam (of referrer spam): wat is dat? Hoe stop je het?

In SQL:

MariaDB [(none)]> select 'x'='x';
+---------+
| 'x'='x' |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

Zoals gezegd: Dit geldt niet alleen voor GET-variabelen, maar ook POST- en REQUEST-variabelen. Een POST-variable komt vanuit een formulier en wordt via de methode POST verstuurd naar de server. Een REQUEST-variabele bevat de inhoud van GET, POST en COOKIE variabelen.

Een tweede SQL-injection voorbeeld: SLEEP()

Als een kwaadwillende een SLEEP()-commando injecteert in de MySQL query of opdracht, dan slaapt MySQL het opgegeven aantal seconden na ieder gevonden record. Bijvoorbeeld:

$id = $_GET['id'];
$query = "SELECT * FROM `tabel` WHERE id='$id'";

De aanvaller kan '$id' in de query overschrijven met door het toevoegen van een extra ':

5' AND SLEEP(5) AND '1

Hiermee wordt de query:

SELECT * FROM `tabel` WHERE id='5' AND SLEEP(5) AND '1'

Dus zodra id=5 gevonden is, slaapt MySQL 5 seconden. Want dat is wat aan MySQL opgedragen is.

Niet heel erg zou je denken, maar stel je nou eens voor: in plaats van een (gebruikers)id, zoeken we, in ons blog, naar alle blogposts van de gebruiker Admin:

SELECT * FROM `blog` WHERE auteur='Admin'

Als dat 300 posts oplevert, en de aanvaller weet hierin weer een SLEEP(5) te injecteren, dan slaapt MySQL in totaal 1500 seconden! Voor iedere keer dat deze SELECT uitgevoerd wordt. Een kwaadwillende doet dat natuurlijk heel vaak.

Door een SLEEP()-aanval kan een MySQL-databaseserver snel door alle beschikbare verbindingen (sockets) heen zijn, en kunnen geldige MySQL-verbindingen en query’s niet meer uitgevoerd worden. De aanvaller heeft succesvol een Denial-of-Service (DoS) aanval uitgevoerd!

Hoe kun je SQL-injection tegengaan?

Je begrijpt nu waarom SQL-injection onwenselijk is, en dat een website hiertegen beveiligd moet worden. Gelukkig is dat relatief eenvoudig. Deze SQL-injection aanvallen gaan we op drie manieren tegen, met PHP-code als voorbeeld.

“cast” data-typen

Het casten van data-typen wil zeggen: bepaal van een variabele wat voor type het is. Is het een integer, een string of boolean (true/false)? Geef dat op bij het aanmaken van de variabele. Het SLEEP()-voorbeeld is eenvoudig te beveiligen door vooraf te bepalen dat id een integer is:

$id = (int)$_GET['id'];

Wanneer er nu 5' AND SLEEP(5) AND '1' ingevuld wordt kan dit niet in $id gestopt worden, omdat het geen getal (of integer) is. Je vindt een overzicht met data-typen, en informatie over het “casten” op http://php.net/manual/en/language.types.type-juggling.php.

Ook interessant voor jou:  PHP optimaliseren op Windows Server IIS

Escape strings

Gebruik de functie mysqli_real_escape_string() om speciale karakters, waaronder het ' teken, te escapen. Een karakter wordt dan voorzien van een extra , waarmee het effect van een ' ongedaan wordt gemaakt:

SELECT * FROM `tabel` WHERE id='5' AND SLEEP(5) AND '1'

Hiermee is de SQL injection succesvol tegengegaan.

Merk hierbij twee zaken op:

  1. we gebruiken hier mysqli_real_escape_string, met de i van improved, en niet mysql_real_escape_string. Die laatste komt voort uit de ext/mysql API en is verouderd. Gebruik alléén nog MySQLi of PDO om te communiceren met een MySQL-database vanuit PHP.
  2. het gebruik van mysqli_real_escape_string gaat SQL-injection tegen, maar is een van de minst goede maatregelen tegen SQL-injection. Het beveiligt niet in alle gevallen de query.

Prepared Statements in SQL

Eén van de beste beveiligingen tegen SQL-injection is het gebruik van Prepared Statements (vaak afgekort met “PS”). Door prepared statements te gebruiken wordt de structuur van een query vooraf vastgelegd en kan niet meer veranderd worden.

Als je de MySQLi-extensie van PHP gebruikt voor het communiceren met de database, dan gebruik je de functie stmt_init() om een prepared statement object aan te maken. Die voer je vervolgens uit met $stmt->prepare, waarna je query-structuur kunt vastleggen. PHP heeft hier goede en duidelijke documentatie over: http://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php.

In ASP kun je het ADODB.Command object gebruiken voor het prepareren van jouw queries en statements.

Prepared statements met PHP MySQLi

In PHP kun je prepared statements gebruiken via de MySQLi-extensie, of MySQL Improved Extension. Deze MySQLi-extensie is veelal standaard ingeschakeld en beschikbaar.

Een eenvoudig PHP MySQLi prepared statement voorbeeld is:

$conn = new mysqli( "localhost", "user", "pass", "db" );
$query = $conn>prepare( "SELECT gegeven FROM tabel WHERE id = ?" );
$query>bind_param( "i", $id );
$query->execute();
...

Het eerste argument van bind_param is het type. Hier wordt een i gebruikt voor Integer, en ook zijn mogelijk: s voor String, d voor double en b voor BLOB.

Ook interessant voor jou:  SQL injection tegengaan met prepared statements

Ondanks dat het bovenstaande voorbeeldje uitgaat van MySQLi en een MySQL-database moet je altijd invoer valideren, voor alle databasetypen!

De post over het tegengaan van cross site scripting geeft meer uitleg over invoervalidatie.

Een laatste tip: netheid

Als je netjes programmeert is de kans op fouten en slordigheidjes kleiner. En dus de kans op misbruik ook. Gebruik je variabelen in MySQL query’s? Omsluit ze met accolades, {$var}:

$query = "SELECT * FROM `tabel` WHERE id='{$id}'";
$query = "SELECT * FROM `Login` WHERE gebruikersnaam = '{$gebruikersnaam}' and wachtwoord = '{$wachtwoord}'";

Het gebruik van accolades om variabelen zorg ervoor dat hetgeen binnen de accolades als variabele wordt gezien, en dat wat erbuiten staat niet. Het voordeel hiervan is dat een statement met {$gebruikersnaam}en nog iets wel werkt. Een statement met $gebruikersnaamen nog iets faalt omdat $gebruikersnaamen niet bestaat.

Het voorkomt dus dubbelzinnigheid, en is zeker met dynamisch samengestelde strings een manier om complexe fouten te voorkomen. Complexe fouten die anders wellicht moeilijk traceerbaar zijn.

Gebruik ook backticks (`) om tabel- en veldnamen:

SELECT * FROM `tabel` where `id` = '{$id}';

Bij gereserveerde namen in MySQL is het gebruik van backticks (accent grave) al noodzakelijk, leer je daarom aan dit overal te doen als goede gewoonte.

  • Strings quoten in query’s: noodzakelijk
  • Backticks om veldnamen: aan te raden, bij gereserveerde namen noodzakelijk
  • Accolades om variabelen namen: optioneel, zelden noodzakelijk, maar een goede gewoonte

“Want to say thanks?”

Heeft dit artikel je geholpen met het oplossen van een probleem? Vond je deze post interessant? Waarom doneer je dan geen kopje koffie? 🙂

Een kleine donatie van slechts €5 helpt mij enorm in de ontwikkeling en onderzoek van posts, en hosting van dit blog.

Koop een kop koffie

Bedankt voor je support.