Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Tips voor een veiligere site

Website-veiligheid is momenteel een hot-topic in PHP land. De nodige opensource pakketten zijn de laatste tijd negatief in het nieuws geweest in verband met beveiligingsproblemen. Daarom hier een paar tips waarmee je je eigen site wat veiliger kunt maken.


Update 09/04/06: Zoals Peter R in de comments juist aanwijst bevat de code in dit script geen fout afhandeling. Dit is bewust gedaan om de relevante code zo kort en zo duidelijk mogelijk te houden. Dit houdt natuurlijk niet in dat je zelf ook geen foutafhandeling moet implementeren!

Controleer waar gegevens vandaan komen.

Vroegâh, registreerde PHP alle binnenkomende waarden direct als variabele in het globale bereik van een script. Deze feature heet "register_globals". Opzich klinkt het best handig, je kon er meteen mee aan de slag. De keerzijde is alleen dat als je niet heel erg op past, het onveilige situaties kan veroorzaken:

Onveilig:

PHP:
  1.  
  2. // $isIngelogd is op de login pagina
  3. // aan de sessie toegevoegd
  4. if ($isIngelogd) { 
  5.   echo "Hoi admin!";
  6. }

Deze code is veilig zolang er inderdaad een variabele $isIngelogd in de sessie bestaat. PHP registreert de binnenkomende variabelen namelijk op een bepaalde volgorde waarbij de sessie waarden standaard als laatste worden geladen. Als er dus al een variabele $isIngelogd bestaat, zou deze overschreven worden.

Maar wanneer de pagina wordt opgevraagd zonder dat de waarde $isIngelogd bestaat in de sessie kun je ineens via de URL een waarde voor $isIngelogd doorgeven: beheer.php?isIngelogd=true. PHP registreert $isIngelogd eerst met de waarde 'true' en omdat er geen sessie variabele met de naam $isInlogd is, wordt de waarde niet overschreven. De user wordt nu ineens als ingelogd beschouwd!

Beter:

PHP:
  1.  
  2. if (isset($_SESSION['isIngelogd'])
  3.         && $_SESSION['isIngelogd'] === true)
  4. {
  5.   echo "Hoi admin!";
  6. }

Door gebruik te maken van de super globals kun je precies aangeven waar gegevens vandaan moeten komen. In de bovenstaande code zie je meteen dat we controleren of isIngelogd bestaat in de sessie, niet in een algemene variabele. Sinds PHP 4.2.0 staat de "register_globals" functionaliteit standaard uit en ondanks dat het mogelijk is om dit weer te activeren raad ik je aan om gebruik te maken van deze superglobals.

Controleer in welke staat gegevens zich bevinden

Magic quotes is een PHP feature die alle inkomende data controleert of er soms aanhalingstekens in staat, en zoja, dan "escaped" PHP ze. Dit houdt in dat er een \ voor de aanhalingstekens worden gezet. Op deze manier kan de data in principe veilig gebruikt worden in SQL queries en HTML weergave. Echter, sommige installaties van PHP hebben magic quotes uitstaan, andere hebben het juist aanstaan. Als je op magic quotes vertrouwd voor veiligheid, terwijl deze op jouw server juist uitstaan, is er een veiligheidsprobleem:

Onveilig:

PHP:
  1. // We vertrouwen er op dat magic quotes aan staat!
  2. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  3.   if (isset($_POST['username'])) {
  4.     $username = $_POST['username'];
  5.  
  6.     mysql_query("SELECT * FROM users WHERE username = '$username'");
  7.   } else {
  8.     /* ... het emailadres was uberhaupt niet beschikbaar ... */
  9.   }
  10. }

Deze code is onveilig omdat we niet zeker weten of de waarde in $username beveiligd is door "magic quotes" of niet. Als magic quotes uitgeschakeld is, is de gebruiker in staat om de SQL query te manipuleren en zo mogelijk andere resultaten te veroorzaken.

Stel nu dat de gebruiker test' OR level = 'admin invult als gebruikersnaam. De gegenereerde SQL query wordt dan: "SELECT * FROM users WHERE username = 'test' OR level ='admin'". De SQL query selecteert dan dus alle users waar de username 'test' is OF alle users waar het userlevel op 'admin' staat. Op deze wijze zou een compleet inlog systeem dus omzeilt kunnen worden. Dit fenomeen heet SQL injectie.

Beter:

PHP:
  1. /* Controleer of de magic quotes feature
  2. * actief is en zo ja, maak dan het effect
  3. * ongedaan. */
  4.  
  5. if ($_SERVER['REQUEST_METHOD'] == 'POST')
  6. {
  7.   if (isset($_POST['email'])) {
  8.     $email = $_POST['email'];
  9.  
  10.     if (get_magic_quotes_gpc()) {
  11.       $email = stripslashes($email);
  12.     }
  13.  
  14.     mysql_query("INSERT INTO klanten (email) VALUES ('$email')");
  15.   } else {     
  16.      /* ... het emailadres was uberhaupt niet beschikbaar ... */   
  17.   }
  18. }

Nu je zeker weet dat de gegevens zich in een bepaalde staat verkeren, weet je ook zeker dat je wanneer je deze gegevens weer gaat gebruiken in bijvoorbeeld een SQL query of als HTML weergave, je zelf de nodige veiligheids maatregelen zult moeten nemen omdat je anders in de problemen raakt.

Een goede manier is om standaard een array te gebruiken met een duidelijke naam heeft, welke aangeeft dat de gegevens in die array nog niet beveiligd zijn, bijvoorbeeld $onveiligeData. Wanneer jij $onveiligeData gebruikt ziet worden in een SQL query weet je meteen dat het fout zit.

PHP:
  1. class HttpUtility
  2. {
  3.   function haalPostData($pVeldNamen)
  4.   {
  5.     $onveiligeData = array();
  6.  
  7.     foreach ($pVeldNamen as $naam)
  8.     {
  9.       if (isset($_POST[$naam]))
  10.       {
  11.         $onveiligeData[$naam] = $_POST[$naam];
  12.  
  13.         if (get_magic_quotes_gpc()) {
  14.           $onveiligeData[$naam] = stripslashes($onveiligeData[$naam]);
  15.         }
  16.       } else {
  17.         $onveiligeData[$key] = "";
  18.       }
  19.     }
  20.  
  21.     return $onveiligeData;
  22.   }
  23. }
  24.  
  25. if ($_SERVER['REQUEST_METHOD'] == 'POST')
  26. {
  27.   $onveiligeVelden = array ( 'email' );
  28.   $onveiligeData   = HttpUtility::haalPostData($onveiligeVelden);
  29.  
  30.   /* Je ziet nu direct dat er hier iets fout is: we voeren
  31.    * onveilige data direct aan mysql_query() en dat
  32.    * kan nooit de bedoeling zijn! */
  33.    
  34.   mysql_query("INSERT INTO klant (email) values ('{$onveiligeData['email']}')");
  35. }

Verifieer de juistheid van binnenkomende gegevens

Alle gegevens die je binnenkrijgt via een formulier is in feite een string. Echter, wanneer je een database ID verwacht is het natuurlijk wel belangrijk dat je zeker weet dat dit een nummer is en niet een string met mogelijk ongewenste dingen daarin:

Onveilig:

PHP:
  1. if ($_SERVER['REQUEST_METHOD'] == 'POST')
  2. {
  3.   $klantID = $_POST['klantID'];
  4.   mysql_query("SELECT * FROM klant WHERE id = $klantID");
  5. }

Beter:

PHP:
  1. if ($_SERVER['REQUEST_METHOD'] == 'POST')
  2. {
  3.   /* Haal de gegevens op via onze functie, zodat we weten in wat voor staat
  4.    * de data verkeerd. */
  5.  
  6.   $onveiligeVelden = array ('klantID');
  7.   $onveiligeData   = HttpUtility::haalPostData($onveiligeVelden);
  8.  
  9.   /* Controleer met behulp van de PHP functie is_numeric of
  10.    * de gegeven klantID waarde wel echt een nummer is. */
  11.   if (is_numeric($onveiligeData['klantID']))
  12.   {
  13.     /* Als de waarde een nummer is, wijs deze dan
  14.      * toe aan een variabele, zodat we weten dat
  15.      * dit geen onveilige data meer is en
  16.      * gebruik deze vervolgens in de SQL query. */
  17.  
  18.     $klantID = $onveiligeData['klantID'];
  19.     mysql_query("SELECT * FROM klant WHERE id = $klantID");
  20.  
  21.   } else { 
  22.     echo "Zeg, wijsneus, dat is geen nummer!";
  23.     exit;
  24.   }
  25. }

Beveilig "uitgaande" gegevens

Wanneer je formulieren gebruikt op een website gebeuren er meestal 2 dingen met de gegevens die worden ingevuld: ze worden in een database geplaatst en vaak nog eens weergegeven.

Onveilig:

PHP:
  1. if ($_SERVER['REQUEST_METHOD'] == 'POST')
  2. {
  3.   $onveiligeVelden = array ('klantNaam');
  4.   $onveiligeData   = HttpUtility::haalPostData($onveiligeVelden);
  5.  
  6.   if (!empty($onveiligeData['klantNaam']))
  7.   {
  8.     $klantNaam = $onveiligeData['klantNaam'];
  9.     mysql_query("INSERT INTO klant (naam) values('$klantNaam')");
  10.    
  11.     echo "$klantNaam is toegevoegd aan de database<br />";
  12.   } else {
  13.     echo "Zeg, wijsneus, dat is geen nummer!";
  14.     exit;
  15.   }
  16. }

Hoewel we hier al veel stappen hebben ondernomen om te zorgen dat de gegevens veilig zijn te gebruiken, zijn we vergeten
om de klant naam te beveiligen tegen SQL injectie en tegen cross site scripting.

Beter:

PHP:
  1. if ($_SERVER['REQUEST_METHOD'] == 'POST')
  2. {
  3.   $onveiligeVelden = array ('klantNaam');
  4.   $onveiligeData = HttpUtility::haalPostData($onveiligeVelden);
  5.  
  6.   if (!empty($onveiligeData['klantNaam'])) {
  7.     /* Beveilig de data tegen SQL injectie */
  8.     $klantNaamSql = mysql_real_escape_string($onveiligeData['klantNaam']);
  9.  
  10.     /* Beveilig de data tegen Cross Site Scripting door
  11.      * eventuele HTML karakters om te zetten naar
  12.      * tekst elementen */
  13.  
  14.     $klantNaamHtml = htmlspecialchars($onveiligeData['klantNaam']);
  15.  
  16.     mysql_query("INSERT INTO klant (naam) values('$klantNaamSql')");
  17.     echo "$klantNaamHtml is toegevoegd aan de database<br />";
  18.   } else {
  19.     echo "Zeg, wijsneus, dat is geen nummer!";
  20.     exit;
  21.   }
  22. }

Hier gebruiken we ook een specifieke variabele naam voor (maar nu juist voor veilige gegevens) die aangeeft of de waarde voor een bepaald doeleinde gebruikt kan worden.

Conclusie

Dit waren een paar tips waarmee je je site wat veiliger kunt maken.

Reageer ook!

[...] In dit artikeltje keken we naar een paar standaard dingen die vaak fout gaan bij mensen die met PHP beginnen. In deze korte toevoeging kijken we even naar wat variabelen in $_SERVER en hoe je die zou moeten gebruiken. [...]

Wat krijgen we nou? Een website als dit en dan alsnog de variable tussen quotes zetten? tss...

@peter
Ja inderdaad en die class is ook totaal onnodig.
Een functie is genoeg.

Verder is er ook geen foutafhandeling.
Artikel moet dus wel wat aangepast worden!

Het gebruik van variables in quotes kan me niet zo boeien. Het performance verschil in een normaal (!) script is verwaarloosbaar. Zie ook http://www.scriptorama.nl/algemeen/quotes-battle . Desondanks gebruik ik in mijn eigen scripts meestal gewoon single quotes, go figure. Hoe dan ook, dit is niet waar het artikel over gaat.

@Peter R: Wat betreft de klasse, je hebt gelijk. Hij is inderdaad niet perse nodig en wordt hier voornamelijk gebruikt als een soort namespacing mechanisme.

De code in dit artikel heeft geen (MySQL) fout afhandeling omdat dat niet ontzettend veel met dit bewuste artikel te maken heeft. De extra code die nodig is voor foutafhandeling kan afleiden van het punt dat ik probeer te maken.

Normaal gesproken zet ik er bij dat ik bewust de fout afhandeling achterwege laat, maar het lijkt er op dat ik dat hier vergeten ben. Ik zal dat alsnog toevoegen.

Ik ben wel van plan om nog een nieuwere uitgebreidere versie van dit artikel te schrijven. Daarin zal die HttpUtility klasse zeker nog eens aangepast worden. Als jullie nog suggesties hebben daarvoor hoor ik dat graag. Inbox staat open ;-)

[...] Dit biedt natuurlijk wat mogelijkheden. In Tips voor een veiligere website gebruik ik bijvoorbeeld de prefix 'onveilig' maar ook de suffix Html en Sql om aan te geven in wat voor context deze variabelen wel of niet gebruikt mogen worden. Nu zijn deze complete woorden niet bijzonder praktisch. Spolsky zelf geeft al aan dat je de prefix 'us' van Unsafe String kunt gebruiken voor gegevens die niet zonder eerst gecontroleerd te zijn naar de browser gestuurd mogen worden. Aangezien security iets verder gaat dan alleen cross-site-scripting is het misschien juist wel slim om een iets uitgebreidere set prefixen te gebruiken hiervoor: [...]

Welke aap heeft dit gescrheven:

1.
/* Controleer of de magic quotes feature
2.
* actief is en zo ja, maak dan het effect
3.
* ongedaan. */
4.

5.
if ($_SERVER['REQUEST_METHOD'] == 'POST')
6.
{
7.
if (isset($_POST['email'])) {
8.
$email = $_POST['email'];
9.

10.
if (get_magic_quotes_gpc()) {
11.
$email = stripslashes($email);
12.
}
13.

14.
mysql_query("INSERT INTO klanten (email) VALUES ('$email')");
15.
} else {
16.
/* ... het emailadres was uberhaupt niet beschikbaar ... */
17.
}
18.
}

Even voor de duidelijkheid. ALS magic_quotes_gpc AAN staat, maak je het ongedaan dmv stripslashes() en dan gooi je het de query in!? DAT is veilig??? Een NIET geslashte waarde erin??? Wanne randapen lopen hier rond dan!
Andersom lijkt me toch IETSJE slimmer:

if ( !get_magic_quotes_gpc() )
{
$email = addslashes($email);
}

Dan gaat het sowieso geslasht de query in.
Dit is dan een artikel over beveiliging! OMG!

Het artikel is incrementeel opgezet. Onder ieder kopje worden concepten toegepast die bij het vorige kopje werden uitgelegd. Daarbij is ieder blokje waar 'Beter': boven staat steeds iets veiliger dan het vorige. Het laatste code blok mag dus als 'veilig' beschouwd worden.

Misschien enigzins ondoorzichtig, inderdaad.

Onder het kopje 'Beveilig "uitgaande" gegevens' wordt de situatie die jij aanhaalt besproken.

@Rudie,
beetje minder onbeschoft en asociaal mag ook wel.

Magic quotes klooi je niet mee, die staan gewoon standaard uit.

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>