Elke regel input van buitenaf is een potentieel risico. Validatie en security in PHP zijn daarom geen optionele extra's, maar de fundering van elke betrouwbare webapplicatie. Of je nu een simpel contactformulier bouwt of een complete API, je moet ervan uitgaan dat gebruikers (en aanvallers) alles proberen om je systeem te breken.
In dit artikel leer je de essentiële security-basics die elke PHP-developer moet kennen. Van input validatie en prepared statements tot XSS-bescherming, CSRF-tokens en veilige wachtwoordopslag. Geen theoretisch verhaal, maar praktische code die je direct kunt toepassen.
Waarom security vanaf dag één belangrijk is
Veel beginnende developers zien security als iets dat je later wel fixt. Dat is een gevaarlijke mindset. Een enkele SQL injection of XSS-kwetsbaarheid kan leiden tot gestolen data, gekaapte accounts of een volledig gecompromitteerde server.
De meeste aanvallen zijn geautomatiseerd. Bots scannen continu het internet op kwetsbare PHP-applicaties. Een verkeerd geconfigureerd formulier kan binnen uren worden gevonden en misbruikt.
Het goede nieuws: de meeste kwetsbaarheden zijn eenvoudig te voorkomen als je de basis begrijpt. Als je de concepten uit Werken met databases in PHP met PDO en Variabelen en control flow onder de knie hebt, heb je de bouwstenen al in huis.
Input validatie: vertrouw niemand
De eerste regel van security is simpel: trust no input. Alles wat van buiten je applicatie komt, formulierdata, URL-parameters, cookies, headers, bestanden, moet gevalideerd worden voordat je er iets mee doet.
Validatie vs sanitization
Er is een belangrijk verschil tussen deze twee concepten:
- Validatie controleert of input voldoet aan de verwachte vorm (is dit een geldig e-mailadres?)
- Sanitization verwijdert of escapet gevaarlijke karakters (strip HTML-tags uit deze tekst)
Je hebt meestal beide nodig, maar valideer eerst. Als de input niet geldig is, weiger hem gewoon.
Filter-functies gebruiken
PHP heeft een ingebouwde filter_var() functie die veel standaard-validaties uit handen neemt:
$email = $_POST['email'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
die('Ongeldig e-mailadres');
}
$leeftijd = filter_var($_POST['leeftijd'] ?? '', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 120]
]);
if ($leeftijd === false) {
die('Ongeldige leeftijd');
}
$url = filter_var($_POST['website'] ?? '', FILTER_VALIDATE_URL);
Type-validatie met moderne PHP
In moderne PHP kun je veel validatie afvangen via type declarations en strict types:
declare(strict_types=1);
function maakGebruiker(string $naam, int $leeftijd, string $email): void
{
if (strlen($naam) < 2 || strlen($naam) > 100) {
throw new InvalidArgumentException('Naam moet tussen 2 en 100 tekens zijn');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Ongeldig e-mailadres');
}
// Doorgaan met de logica
}
Door declare(strict_types=1) te gebruiken, voorkom je dat PHP stilletjes types converteert. Meer hierover lees je in ons artikel over functies en scope.
SQL injection voorkomen
SQL injection is één van de bekendste en gevaarlijkste aanvallen. Het werkt doordat aanvallers SQL-commando's kunnen injecteren in queries die gebruikersinput bevatten.
De gevaarlijke manier
Dit is code die je nooit moet schrijven:
// NOOIT DOEN
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $id";
$result = $pdo->query($query);
Een aanvaller kan ?id=1 OR 1=1 aanroepen en alle gebruikers ophalen. Of erger: ?id=1; DROP TABLE users--.
De veilige manier: prepared statements
Gebruik altijd prepared statements met bound parameters:
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $_GET['id']]);
$user = $stmt->fetch();
PDO zorgt dat de parameter veilig wordt behandeld, los van de SQL-structuur. Een uitgebreide uitleg vind je in onze PDO gids.
Als je een framework zoals Laravel gebruikt, doet de query builder dit automatisch voor je. Maar zelfs dan is het belangrijk om te snappen wat er onder de motorkap gebeurt.
XSS: Cross-Site Scripting voorkomen
XSS-aanvallen werken door JavaScript te injecteren in je website via ongeëscapete output. Als een gebruiker een comment plaatst met <script>alert('hacked')</script> en jij toont die direct, voert elke bezoeker dat script uit.
Output escapen met htmlspecialchars
De gouden regel: escape bij output, niet bij input. Sla ruwe data op in je database en escape pas wanneer je het toont.
<?php $gebruikersnaam = $_SESSION['naam']; ?>
<!-- FOUT: kwetsbaar voor XSS -->
<p>Hallo <?= $gebruikersnaam ?></p>
<!-- GOED: veilig geëscapet -->
<p>Hallo <?= htmlspecialchars($gebruikersnaam, ENT_QUOTES, 'UTF-8') ?></p>
Maak een helper-functie om dit korter te schrijven:
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
// Gebruik:
echo '<p>Hallo ' . e($gebruikersnaam) . '</p>';
Context matters
Escaping hangt af van de context waar je data gebruikt:
- HTML body →
htmlspecialchars() - HTML attribuut →
htmlspecialchars()metENT_QUOTES - JavaScript →
json_encode()metJSON_HEX_TAGenJSON_HEX_AMP - URL →
urlencode()ofrawurlencode() - CSS → vermijd user input in CSS, of gebruik een whitelist
Content Security Policy
Als extra laag bescherming kun je een Content Security Policy instellen via HTTP-headers. Dit voorkomt dat inline scripts of externe scripts van niet-vertrouwde bronnen worden uitgevoerd, zelfs als er een XSS-kwetsbaarheid is.
CSRF-tokens: bescherm je formulieren
Cross-Site Request Forgery (CSRF) is een aanval waarbij een aanvaller een ingelogde gebruiker ongemerkt acties laat uitvoeren op jouw site. Bijvoorbeeld door een verborgen formulier op een andere website te plaatsen.
CSRF-tokens implementeren
De oplossing is een uniek, onvoorspelbaar token per sessie (of per formulier) dat je valideert bij elke POST-request:
session_start();
// Token genereren bij het tonen van het formulier
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
In je formulier:
<form method="POST" action="/update-profiel">
<input type="hidden" name="csrf_token" value="<?= e($_SESSION['csrf_token']) ?>">
<!-- overige velden -->
<button type="submit">Opslaan</button>
</form>
Bij het verwerken:
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '')) {
http_response_code(403);
die('Ongeldige CSRF-token');
}
Gebruik hash_equals() in plaats van === om timing attacks te voorkomen. Frameworks zoals Laravel regelen dit automatisch voor je.
Wachtwoorden veilig opslaan
Wachtwoorden in platte tekst opslaan is een doodzonde. Zelfs "simpele" hashing met MD5 of SHA1 is niet veilig genoeg, die algoritmes zijn te snel te bruteforcen.
password_hash en password_verify
PHP biedt sinds versie 5.5 een veilige, toekomstbestendige API:
// Wachtwoord opslaan bij registratie
$wachtwoord = $_POST['wachtwoord'];
$hash = password_hash($wachtwoord, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (?, ?)');
$stmt->execute([$email, $hash]);
// Inloggen controleren
$stmt = $pdo->prepare('SELECT password FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($_POST['wachtwoord'], $user['password'])) {
// Inlog succesvol
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
} else {
// Gebruik altijd dezelfde foutmelding
die('Ongeldige inloggegevens');
}
PASSWORD_DEFAULT gebruikt het huidige aanbevolen algoritme (bcrypt, binnenkort argon2). PHP update dit automatisch bij nieuwe versies, zonder dat je code hoeft te veranderen.
Rehash wanneer nodig
Controleer periodiek of een hash geüpdatet moet worden naar een nieuwer algoritme:
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
$nieuweHash = password_hash($_POST['wachtwoord'], PASSWORD_DEFAULT);
// Sla de nieuwe hash op
}
Sessies veilig beheren
Sessies zijn vaak het doelwit van aanvallen. Een paar configuratie-instellingen maken je sessies aanzienlijk veiliger:
ini_set('session.cookie_httponly', 1); // Niet toegankelijk via JavaScript
ini_set('session.cookie_secure', 1); // Alleen over HTTPS
ini_set('session.cookie_samesite', 'Strict'); // CSRF-bescherming
ini_set('session.use_strict_mode', 1); // Weiger onbekende sessie-IDs
session_start();
Regenereer de sessie-ID na inloggen of rechten-wijzigingen:
session_regenerate_id(true);
Dit voorkomt session fixation, waarbij een aanvaller een gebruiker met een bekende sessie-ID laat inloggen.
Error handling: lek geen informatie
In productie mag je nooit ruwe foutmeldingen tonen. Een stack trace kan paden, database-namen of codestructuur verraden die een aanvaller helpen.
// In productie
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/errors.log');
error_reporting(E_ALL);
Toon gebruikers een generieke foutpagina en log de details server-side. Wanneer je een API bouwt in PHP is dit extra belangrijk, geef nooit database-errors terug in JSON-responses.
Een security checklist
Voordat je code naar productie gaat, loop deze punten langs:
- Alle input gevalideerd met
filter_var()of strikte type checks - Alle database-queries gebruiken prepared statements
- Alle output geëscaped met
htmlspecialchars()of context-juiste escaping - Elk formulier heeft een CSRF-token
- Wachtwoorden gehashed met
password_hash() - HTTPS afgedwongen via HSTS-headers
- Sessies geconfigureerd met HttpOnly, Secure en SameSite
- Foutmeldingen gelogd, niet getoond
- Dependencies up-to-date via Composer (zie Composer gids)
- Beveiligingsheaders ingesteld (CSP, X-Frame-Options, X-Content-Type-Options)
De OWASP Top 10 is een goede referentie voor de meest voorkomende webkwetsbaarheden en hoe je ze voorkomt.
Conclusie
Security hoeft niet overweldigend te zijn. Als je een paar principes consequent toepast, valideer input, gebruik prepared statements, escape output, hash wachtwoorden en bescherm je formulieren, voorkom je de overgrote meerderheid van aanvallen.
Begin klein: maak het een gewoonte om bij elke $_POST, $_GET of database-interactie even stil te staan bij security. Hoe meer je het doet, hoe natuurlijker het voelt. En onthoud: het is veel makkelijker om security vanaf het begin in te bouwen dan om het er achteraf bij te plakken.
Veelgestelde vragen
Wat is het verschil tussen validatie en sanitization in PHP?
Validatie controleert of input voldoet aan de verwachte regels (bijvoorbeeld een geldig e-mailadres). Sanitization verwijdert of neutraliseert gevaarlijke tekens uit input. Je hebt meestal beide nodig, maar validatie komt eerst.
Hoe voorkom ik SQL injection in PHP?
Gebruik altijd prepared statements met PDO of MySQLi. Voeg nooit gebruikersinvoer direct samen in een SQL-query met string concatenatie, ook niet als je denkt dat de input veilig is.
Wat is XSS en hoe bescherm ik mijn site ertegen?
XSS (Cross-Site Scripting) is een aanval waarbij kwaadaardige scripts in je pagina worden geïnjecteerd. Bescherm je site door alle output te escapen met htmlspecialchars() voordat je data in HTML weergeeft.
Heb ik CSRF-bescherming nodig voor elk formulier?
Ja, elk formulier dat een actie uitvoert (zoals data opslaan, wijzigen of verwijderen) moet een CSRF-token gebruiken. Alleen GET-requests die niets muteren hebben het niet nodig.
Moet ik wachtwoorden hashen of encrypten?
Hash wachtwoorden altijd met password_hash() en controleer ze met password_verify(). Encryptie is omkeerbaar en daarom ongeschikt voor wachtwoorden, hashing is dat niet.