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 kan een hacker account- en privégegevens stelen. Erg vervelend en onwenselijk natuurlijk. Dit artikel legt je uit hoe je prepared statements in PHP/MySQLi gebruikt om jouw website te beschermen tegen deze aanvallen.

Hoe kan ik mijn website beveiligen tegen SQL-injection aanvallen met prepared statements met PHP en MySQLi?

Het SQL-injection beveiligingsprobleem

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

Door middel van een SQL-injectie-aanval kan een aanvaller zelfs een hele databaseserver ontoegankelijk maken als hij z.g MySQL sleep()-commando’s injecteert. Jij wilt toch niet degene zijn wiens slecht beveiligde website een hele databaseserver met honderden anderen ontoegankelijk geeft gemaakt?

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 SQL 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 = mysqli_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!

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.

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: MySQL 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 door het toevoegen van een extra ':

5' AND SLEEP(5) AND '1

en 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 MySQL opgedragen is om te doen. 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 MySQL 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.

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 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 SQL queries en statements. Je vindt informatie en voorbeelden in mijn post SQL injection tegengaan met prepared 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.

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
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *