Je bent hier: ITFAQ.nl » Beveiliging » SQL injection tegengaan met prepared statements in PHP en ASP

SQL injection tegengaan met prepared statements in PHP en ASP

Het is belangrijk om je website te beveiligen tegen SQL-injection aanvallen. Een SQL injection is een aanvalsmethode om een SQL-database te kraken: door het “injecteren” van SQL-commando’s kan een aanvaller account- en privégegevens stelen, of zelfs een hele databaseserver platleggen. In dit artikel lees je hoe SQL injection werkt, en hoe je het tegengaat met prepared statements in PHP (MySQLi) en in classic ASP.

SQL injectie is verantwoordelijk voor een groot deel van de gehackte websites, waarbij aanvallers waardevolle data van bedrijven en instellingen buitmaken. Denk hierbij aan financiële, maar ook privacygevoelige gegevens van gebruikers, instellingen, overheden, enz. Kortom, een groot gevaar schuilt in SQL injection, en beveiliging hiertegen is belangrijk!

Het SQL-injection beveiligingsprobleem

Ben je bezig met het overstappen van ext/mysql naar MySQLi in PHP? 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 heeft 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( $conn, $query );

Een bezoeker vult zijn gebruikersnaam en wachtwoord in, 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-variabele 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, maakt PHP daar het getal 5 van: alles na het eerste teken dat geen cijfer is valt weg, en de SQL-commando’s komen nooit in de query terecht. Je vindt een overzicht met data-typen, en informatie over het “casten” op php.net.

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 backslash (\), waarmee het effect van een ' ongedaan wordt gemaakt:

$id = mysqli_real_escape_string( $conn, $_GET['id'] );
$query = "SELECT * FROM `tabel` WHERE id='$id'";

Voert een aanvaller nu weer 5' AND SLEEP(5) AND '1 in, dan wordt de query:

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

De hele invoer is nu één tekst tussen aanhalingstekens, en SLEEP(5) wordt niet meer uitgevoerd. 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. De data waarmee het statement later wordt aangeroepen wordt simpelweg als data behandeld en niet als SQL.

Hierbij maakt het niet uit of je gebruikmaakt van ASP, PHP, Perl of ASP.NET als scripttaal, of Microsoft SQL Server, MySQL of PostgreSQL als databaseserver. Invoervalidatie, prepared statements en beveiliging zijn altijd belangrijk!

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. PHP heeft hier goede en duidelijke documentatie over: Prepared Statements.

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.

Prepared statements in classic ASP/VBScript

Let op: classic ASP is inmiddels verouderd. Doe zelf goed onderzoek naar technieken om SQL-injection in jouw website-code tegen te gaan! Je vindt hier een voorbeeld ASP naar MySQL en ASP naar SQL Server connectionstring.

In ASP is een SQL prepared statement een goed middel om SQL injection tegen te gaan. Je gebruikt hiervoor het ADODB.Command object. Een voorbeeld stukje ASP-/VBScript code met een prepared statement is:

<%
<!--#include file="adovbs.inc"-->
Option Explicit
Dim SqlConn, SqlCommand, resultaat
Set SqlConn = Server.CreateObject("ADODB.Connection")

' gebruik {SQL Server} als Driver als onderstaande niet werkt
SqlConn.Open "Driver={ODBC Driver 17 for SQL Server};" & "Server=host;" & _
"DATABASE=databasenaam;" & "uid=gebruikersnaam;" & _
"pwd=wachtwoord;"

Set SqlCommand = Server.CreateObject("ADODB.Command")
Set SqlCommand.ActiveConnection = SqlConn

SqlCommand.CommandType = adCmdText
SqlCommand.CommandText = "SELECT * FROM logintable WHERE userid = ? AND wachtwoord= ?"
SqlCommand.Prepared = True
SqlCommand.Parameters.Append (SqlCommand.CreateParameter("@userid", adVarWChar, adParamInput, Len(Request.Form("user_id")), Request.Form("user_id")))
SqlCommand.Parameters.Append (SqlCommand.CreateParameter("@wachtwoord", adVarWChar, adParamInput, Len(Request.Form("passwd")), Request.Form("passwd")))

Set resultaat = SqlCommand.Execute

SqlCommand.Parameters.Delete("@userid")
SqlCommand.Parameters.Delete("@wachtwoord")
%>

Gebruik, zoals hierboven, Option Explicit om vooraf verplicht variabelen te declareren! Dit vermindert de kans op fouten en slordigheidjes. Je vindt een PHP equivalent in het artikel over variabelen declareren.

Hier wordt het ADODB.Command object gebruikt om een SQL query te prepareren. Voor MySQL is dit niet veel anders.

HTMLEncode

Naast prepared statements is het verstandig om gebruik te maken van de methode Server.HTMLEncode ([2]), om de code verder te beveiligen. Hiermee worden tekens omgezet naar de HTML-geëncodeerde equivalent, wat goed is tegen cross site scripting. Bijvoorbeeld:

<%
Response.Write(Server.HTMLEncode("The image tag: <img>"))
%>

Prepared Statement query-voorbeelden in ASP

Hieronder een aantal prepared statement voorbeelden. Ze zijn verkregen uit het artikel Using Parameterized Queries op Experts Exchange, gepubliceerd door Wayne Barron.

Parameters met tekst VarChar, met een veldlengte van 25

<%
Set chEmail = Server.CreateObject("ADODB.Command")
chEmail.ActiveConnection=objConn
chEmail.Prepared = true
chEmail.commandtext="SELECT cusEmail, password, mydate FROM ordercavecustomer WHERE cusEmail =?"
chEmail.Parameters.Append chEmail.CreateParameter("@cusEmail", adVarChar, adParamInput, 25, loginEmail)
set rschEmail = chEmail.execute
%>

Parameters met een integer (INT – vereist geen lengte)

<%
Set chEmail = Server.CreateObject("ADODB.Command")
chEmail.ActiveConnection=objConn
chEmail.Prepared = true
chEmail.commandtext="SELECT cusEmail, password, myID FROM ordercavecustomer WHERE myID =?"
chEmail.Parameters.Append chEmail.CreateParameter("@myID", adInteger, adParamInput, , getmyID)
set rschEmail = chEmail.execute
%>

Meer dan één parameters, verkregen uit een querystring
Let erop dat je de parameters in de juiste volgorde opgeeft, anders geeft het een fout.

<%
getID = request.QueryString("ID")
getEmail = request.QueryString("Email")
Set chEmail = Server.CreateObject("ADODB.Command")
chEmail.ActiveConnection=objConn
chEmail.Prepared = true
chEmail.commandtext="SELECT cusEmail, password, myID FROM ordercavecustomer WHERE myID =? and cusEmail=?"
chEmail.Parameters.Append chEmail.CreateParameter("@myID", adInteger, adParamInput, , getmyID)
chEmail.Parameters.Append chEmail.CreateParameter("@cusEmail", adVarChar, adParamInput, 25, loginEmail)
set rschEmail = chEmail.execute
%>

Prepared Statement met INSERT
Zorg er weer voor dat alle parameters in de juiste volgorde vermeld staan.

<%
Set chEmail = Server.CreateObject("ADODB.Command")
chEmail.ActiveConnection=objConn
chEmail.commandtext="INSERT into ordercavecustomer(cusEmail, password, myID)values(?,?,?)"
chEmail.Parameters.Append chEmail.CreateParameter("@cusEmail", adVarChar, adParamInput, 25, loginEmail)
chEmail.Parameters.Append chEmail.CreateParameter("@password", adVarChar, adParamInput, 25, loginPass)
chEmail.Parameters.Append chEmail.CreateParameter("@myID", adInteger, adParamInput, , getmyID)
set rschEmail = chEmail.execute
%>

Prepared Statement met UPDATE
Hetzelfde als hiervoor, in de juiste volgorde. De WHERE moet als laatste in de query, en in de parameterlijst.

<%
Set chEmail = Server.CreateObject("ADODB.Command")
chEmail.ActiveConnection=objConn
chEmail.commandtext="update ordercavecustomer set cusEmail=?, password=? where myID=?"
chEmail.Parameters.Append chEmail.CreateParameter("@cusEmail", adVarChar, adParamInput, 25, loginEmail)
chEmail.Parameters.Append chEmail.CreateParameter("@password", adVarChar, adParamInput, 25, loginPass)
chEmail.Parameters.Append chEmail.CreateParameter("@myID", adInteger, adParamInput, , getmyID)
set rschEmail = chEmail.execute
%>

DELETE Statement
Dit voorbeeld verwijdert (DELETE) het item met het opgegeven ID, bijvoorbeeld verkregen uit een querystring

<%
Set chEmail = Server.CreateObject("ADODB.Command")
chEmail.ActiveConnection=objConn
chEmail.commandtext="delete from ordercavecustomer where myID=?"
chEmail.Parameters.Append chEmail.CreateParameter("@myID", adInteger, adParamInput, , getmyID)
set rschEmail = chEmail.execute
%>

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 zorgt 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. Let op: accolades maken een query niet veiliger, dat doe je met de maatregelen hierboven.

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

Conclusie SQL injection voorkomen

Zoals je kunt lezen is beveiliging tegen SQL injection heel erg belangrijk. Niet alleen kunnen kwaadwillende derden vaak eenvoudig inbreken op jouw website en SQL-database, met alle privacygevolgen voor gebruikers van dien. Een SQL injection door middel van sleep()-aanvallen kan zélfs een Denial-of-Service (DoS) veroorzaken en alles onderuit trekken! Daarvan wil jij toch niet de veroorzaker zijn? Gebruik daarom prepared statements, in PHP met MySQLi (of PDO) en in ASP met ADODB.Command, en valideer altijd de invoer.

1 gedachte over “SQL injection tegengaan met prepared statements in PHP en ASP”

Reacties zijn gesloten.

Scroll naar boven