SQL-injectie is een van de gevaarlijkste beveiligingslekken in webapplicaties. Het ontstaat wanneer gebruikersinvoer direct in een SQL-query wordt geplaatst zonder bescherming. Een aanvaller kan dan eigen SQL-code invoegen en je database manipuleren.
<?php
// GEVAARLIJK - NOOIT DOEN!
$naam = $_POST['naam']; // Stel: gebruiker voert in: ' OR 1=1 --
$sql = "SELECT * FROM gebruikers WHERE naam = '$naam'";
// Resultaat: SELECT * FROM gebruikers WHERE naam = '' OR 1=1 --'
// Dit geeft ALLE gebruikers terug!
?>
De aanvaller sluit het aanhalingsteken, voegt OR 1=1 toe (altijd waar), en gebruikt -- om de rest van de query als commentaar te markeren.
SQL-injectie kan leiden tot: het lekken van alle data, het wijzigen of verwijderen van data, en in sommige gevallen zelfs het overnemen van de hele server. Dit is de #1 beveiligingsfout in webapplicaties.
Let op: In de oefeningen hieronder hoef je GEEN <?php en ?> tags te typen. Het platform regelt dit automatisch. Je typt alleen de PHP-code zelf.
Bekijk de volgende onveilige code en begrijp het probleem. Maak een variabele $invoer met de waarde "' OR 1=1 --". Bouw de onveilige query "SELECT * FROM users WHERE naam = '$invoer'" en sla deze op in $query. Toon de query met echo zodat je ziet hoe de injectie werkt.
Maak een variabele $invoer met de waarde "'; DROP TABLE users; --". Bouw de onveilige query "SELECT * FROM users WHERE naam = '$invoer'". Toon de query. Dit laat zien hoe een aanvaller een hele tabel kan verwijderen.
De oplossing is simpel: gebruik altijd prepared statements. Hierbij worden de query-structuur en de data strikt gescheiden. PDO verwerkt de data als pure waarden, niet als SQL-code.
<?php
// VEILIG - prepared statement
$stmt = $pdo->prepare("SELECT * FROM gebruikers WHERE naam = :naam");
$stmt->execute([':naam' => $invoer]);
// De invoer wordt als data behandeld, nooit als SQL-code
?>
Het maakt niet uit wat de gebruiker invoert — de database zal het altijd als een waarde behandelen, nooit als SQL-instructies.
Herschrijf de onveilige query "SELECT * FROM users WHERE email = '$email'" naar een veilige versie met prepare() en execute(). Gebruik de named placeholder :email. Toon "Veilige query geschreven".
Herschrijf de onveilige login-query "SELECT * FROM users WHERE naam = '$naam' AND wachtwoord = '$wachtwoord'" naar een veilige versie met twee named placeholders. Toon "Veilige login-query geschreven".
Schrijf een veilige INSERT-query om een nieuwe gebruiker toe te voegen met kolommen naam, email en rol. Gebruik named placeholders. Voer uit met de waarden "Jan", "jan@email.nl", "gebruiker". Toon "Gebruiker veilig toegevoegd".
Schrijf een veilige zoekfunctie zoekGebruikers($pdo, $zoekterm) die gebruikers zoekt met LIKE. De wildcard % hoort in de execute-array, NIET in de query. Return de resultaten. Toon "Zoekfunctie geschreven".
Zelfs met PDO kun je nog fouten maken. Hier zijn de meest voorkomende valkuilen:
<?php
// FOUT 1: Variabele direct in prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = $id"); // FOUT!
// GOED:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
// FOUT 2: Tabelnaam of kolomnaam als placeholder
$stmt = $pdo->prepare("SELECT * FROM :tabel"); // WERKT NIET!
// GOED: Gebruik een whitelist voor dynamische tabel/kolomnamen
$toegestaan = ['users', 'producten', 'bestellingen'];
if (in_array($tabel, $toegestaan)) {
$stmt = $pdo->prepare("SELECT * FROM $tabel");
}
?>
Onthoud: placeholders werken alleen voor waarden, niet voor tabel- of kolomnamen. Voor dynamische tabel- of kolomnamen gebruik je een whitelist.
Schrijf een functie veiligeTabel($tabel) die controleert of de tabelnaam in de whitelist ["users", "producten", "bestellingen"] staat. Zo ja, return de tabelnaam. Zo nee, gooi een Exception met "Ongeldige tabel". Test met "producten" en toon het resultaat.
Schrijf een functie veiligeSortering($kolom, $richting) die controleert of de kolom in ["naam", "prijs", "datum"] staat en de richting "ASC" of "DESC" is. Return de ORDER BY-clausule als string, of gooi een Exception. Test met "prijs" en "DESC".
Bouw een complete veilige zoekpagina. Schrijf een functie zoekProducten($pdo, $zoekterm, $sorteerKolom, $richting) die: 1) de zoekterm veilig verwerkt met een prepared statement en LIKE, 2) de sorteerkolom valideert tegen ["naam", "prijs"], 3) de richting valideert tegen ["ASC", "DESC"]. Als validatie faalt, gebruik standaard "naam" en "ASC". Toon "Veilige zoekpagina geschreven".
prepare() en execute():naam of ?) scheiden data van SQL-structuurin_array()%) in de execute-array, niet in de query