P
imvdmolen.nl
Blog

Beveiliging van Laravel-applicaties tegen cross-site scripting aanvallen

Soms krijg ik een melding binnen van een klant waarvan de contactformulier ineens JavaScript-code begon uit te voeren. Een klassiek geval van cross-site scripting, veroorzaakt doordat er ergens user input ongeëscaped werd weergegeven. Dit soort aanvallen zie ik helaas nog steeds regelmatig langskomen, ondanks dat frameworks zoals Laravel uitstekende bescherming bieden. Het probleem is vaak dat developers niet weten welke beveiligingslagen Laravel precies biedt en wanneer je extra voorzorgsmaatregelen moet nemen.

Cross-site scripting aanvallen ontstaan wanneer kwaadaardige code wordt geïnjecteerd in een webpagina en vervolgens wordt uitgevoerd in de browser van andere gebruikers. Deze aanvallen kunnen leiden tot gestolen sessie-cookies, omleiding naar malafide websites, of het uitvoeren van ongewenste acties namens de gebruiker. Laravel biedt verschillende mechanismen om dit tegen te gaan, maar je moet ze wel correct implementeren.

Automatische escaping in Blade templates

Blade templates zijn ontworpen met beveiliging in gedachten. Telkens wanneer je de dubbele accolades gebruikt, escaped Laravel automatisch de output. Dit gebeurt achter de schermen door htmlspecialchars() aan te roepen met de juiste parameters. Hierdoor wordt potentieel gevaarlijke code omgezet naar veilige HTML-entities.

// Blade template
<p>{{ $userInput }}</p>

// Output
<p>&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</p>

Wat veel developers echter vergeten is dat deze automatische escaping alleen werkt binnen Blade templates en alleen bij het gebruik van dubbele accolades. Zodra je {!! !!} gebruikt, schakelt Blade de escaping uit omdat het ervan uitgaat dat je bewust raw HTML wilt weergeven. Dit kan gevaarlijk zijn als je user input weergeeft zonder extra controles.

De automatische escaping werkt ook niet in JavaScript-context binnen je templates. Als je user input wilt gebruiken in een JavaScript-variabele, moet je extra voorzorgsmaatregelen nemen. Ik gebruik hiervoor vaak de @json directive van Blade, die zorgt voor correcte JSON-encoding en escaping van gevaarlijke karakters.

<script>
    var userData = @json($userInput);
</script>

Deze aanpak voorkomt dat aanvallers JavaScript-code kunnen injecteren door de input als een string te behandelen en alle speciale karakters correct te escapen. Het is een veiliger alternatief dan handmatige concatenatie van strings in JavaScript.

Verschillende soorten XSS-aanvallen herkennen

Reflected XSS is de meest voorkomende vorm die ik tegenkom. Hierbij wordt de kwaadaardige code direct teruggestuurd naar de gebruiker, meestal via URL-parameters. Een typisch voorbeeld is een zoekfunctie die de zoekterm weergeeft zonder deze te escapen.

// Onveilige code
<p>Zoekresultaten voor: <?php echo $_GET['query']; ?></p>

// Aanval
http://example.com/search?query=<script>alert('XSS')</script>

Stored XSS is gevaarlijker omdat de kwaadaardige code wordt opgeslagen in de database en vervolgens wordt weergegeven aan alle gebruikers die de betreffende pagina bezoeken. Dit komt vaak voor in commentaarsecties, gebruikersprofielen of andere plekken waar user-generated content wordt bewaard.

DOM-based XSS vindt plaats volledig in de browser, zonder dat de server betrokken is. JavaScript-code leest bijvoorbeeld een URL-parameter en voegt deze direct toe aan de DOM zonder escaping. Laravel kan hier niet tegen beschermen omdat het geen server-side verwerking behelst.

Geavanceerde validatie en input filtering

Laravels validation system biedt meer dan alleen basiscontroles. Je kunt custom validation rules schrijven die specifiek controleren op potentieel gevaarlijke input. Dit doe ik vaak voor velden waarin HTML toegestaan is, maar alleen specifieke tags.

use Illuminate\Validation\Validator;

$validator = Validator::make($request->all(), [
    'email' => 'required|email',
    'content' => 'required|string|max:1000',
    'title' => ['required', 'string', function ($attribute, $value, $fail) {
        if (preg_match('/<script|javascript:|on\w+=/i', $value)) {
            $fail('Het :attribute veld bevat niet-toegestane inhoud.');
        }
    }],
]);

Voor situaties waar je wel HTML wilt toestaan maar alleen veilige tags, gebruik ik meestal een library zoals HTMLPurifier. Deze kan je integreren in een custom validation rule of als middleware voor specifieke routes. HTMLPurifier analyseert de HTML-structuur en verwijdert alles wat niet op een whitelist staat.

Input sanitatie is een aanvulling op validatie, geen vervanging. Ik pas sanitatie toe voordat ik data opsla in de database, vooral bij velden waarin gebruikers tekst met opmaak kunnen invoeren. PHP's filter_var functie biedt verschillende sanitatie-opties, maar voor complexere gevallen schrijf ik vaak custom functies.

function sanitizeUserInput($input) {
    // Verwijder null bytes
    $input = str_replace("\0", '', $input);
    
    // Normaliseer whitespace
    $input = preg_replace('/\s+/', ' ', trim($input));
    
    // Verwijder potentieel gevaarlijke protocollen
    $input = preg_replace('/javascript:|data:|vbscript:/i', '', $input);
    
    return $input;
}

Content Security Policy implementeren

Content Security Policy is een extra beveiligingslaag die werkt op browser-niveau. Door CSP-headers te sturen, geef je de browser instructies over welke bronnen geladen mogen worden en waar scripts vandaan mogen komen. Deze benadering blokkeert geïnjecteerde scripts zelfs als ze door andere beveiligingslagen heen zijn gekomen.

// In een middleware of controller
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' https://cdnjs.cloudflare.com; style-src \'self\' \'unsafe-inline\'; img-src \'self\' data: https:');

Het configureren van CSP vereist zorgvuldige planning. Je moet alle legitieme bronnen identificeren die je applicatie gebruikt, van externe JavaScript-libraries tot afbeeldingen op CDN's. Een te restrictieve policy breekt functionaliteit, terwijl een te permissieve policy weinig bescherming biedt.

Nonce-based CSP is een geavanceerdere techniek die ik gebruik voor applicaties met hoge beveiligingseisen. Hierbij genereer je voor elke request een unieke nonce en voeg je deze toe aan zowel de CSP-header als aan alle inline scripts en styles. Dit voorkomt dat geïnjecteerde scripts worden uitgevoerd, zelfs als ze erin slagen om in de HTML te komen.

// Genereer nonce
$nonce = base64_encode(random_bytes(16));

// CSP header met nonce
header("Content-Security-Policy: script-src 'self' 'nonce-$nonce'");

// In Blade template
<script nonce="{{ $nonce }}">
    // Veilige inline JavaScript
</script>

Deze aanpak vereist dat je alle inline JavaScript en CSS voorziet van de juiste nonce, maar biedt wel de sterkste bescherming tegen XSS-aanvallen. Het implementeren hiervan doe ik meestal via een custom middleware dat de nonce genereert en beschikbaar maakt voor de views.

XSS-beveiliging is geen eenmalige taak maar een continu proces. Regelmatige security audits, het up-to-date houden van dependencies en het trainen van je team in veilige development practices zijn net zo belangrijk als de technische maatregelen. Elke nieuwe feature die je toevoegt aan je applicatie is een potentieel aanvalsoppervlak dat je moet evalueren en beveiligen.