
Verbeter de beveiliging van je Laravel applicatie met CSP (Content Security Policies)
Met Content Security Policies krijg je meer grip op welke content de browser van je bezoeker inlaadt en uitvoert wanneer zij je webapplicatie bezoeken. In het geval dat je website geïnfecteerd raakt met kwaadwillende content, kan CSP dus helpen om te voorkomen dat die content je bezoekers bereikt. In dit artikel nemen we je mee in de wereld van CSP, en hoe jij het kan toepassen in je Laravel applicatie.
Een Laravel applicatie, net als alle andere webapplicaties, bestaat niet alleen uit html. Ze maken ook gebruik van andere content types, zoals CSS voor de styling en Javascript voor dynamische functionaliteiten. Deze content kan lokaal vanuit je applicatie worden toegevoegd and je pagina's, maar kan ook van buitenaf komen. Denk bijvoorbeeld aan externe diensten zoals Google Analytics, die je eenvoudig kan integreren met een extra script tag. Of een Composer of NPM package. Erg handig en onmisbaar om met minimale middelen veel functionaliteiten aan je webapplicatie toe te voegen. Het brengt echter wel een risico met zich mee. Hoe weet je namelijk dat de software waar je gebruik van maakt altijd te vertrouwen is. Dat is niet te garanderen, maar je kan wel met enige zekerheid weten welke content wél veilig is van zo'n externe bron. Met CSP kan je expliciet aangeven welke soorten content van welke bronnen je wel vertrouwd. Alles wat daar niet toe behoort kan worden geblokkeerd.
Dergelijke aanvallen waarbij gevaarlijke content wordt geïnjecteerd in je webapplicatie heet ook wel Cross Site Scripting (XSS). In het volgende gedeelte zullen we een aantal praktische voorbeelden laten zien van XXS en andere gevaren die CSP helpen te voorkomen of te minimaliseren.
Voorbeelden van risico's die je kan minimaliseren met het toepassen van CSP
Cross Site Scripting
Zoals net besproken kan een kwaadwillend script worden geïnjecteerd vanuit bijvoorbeeld een geïnfecteerde package:
<script src="https://evil.example.com/hacker.js"></script>
Met CSP kan je definiëren welke bronnen je vertrouwd waardoor bovenstaand voorbeeld niet werkt.
Dit hoeven niet enkel scripts te zijn die naar een externe bron leidt, maar het kunnen ook inline scripts zijn.
<script>
console.log("You've been hacked")
</script>
Datzelfde geldt voor javascript in een event handler
<img onmouseover="console.log(`You've been hacked`)" />
En ook bij javascript in een src attribuut van een iframe
<iframe src="javascript:console.log(`You've been hacked`)"></iframe>
Of bij een onveilige browser functies zoals de beruchte eval() functie
eval("console.log(`You've been hacked`)")
In totaal zijn er 28 andere content types waar CSP op toegepast kan worden. Denk bijvoorbeeld ook aan fonts en form actions.
Naast deze voorbeelden van XSS kan je ook met CSP ook Clickjacking voorkomen. Hierbij wordt je webapplicatie in een iframe geplaatst op andere website met het doel om de bezoeker acties bepaalde acties te laten uitvoeren waarvan het zich niet bewust is. Zo kan er met een transparante laag over de betreffende iframe knoppen over jouw web applicatie worden geplaatst zodat het kan lijken voor de bezoeker dat hij rond klikt op jouw webapplicatie maar dus eigenlijk andere acties uitvoert. Met het toepassen van CSP kan je voorkomen dat je webapplicatie via een iframe embedd kan worden.
Hoe wordt CSP toegepast?
Nu je een goed beeld hebt welke gevaren je kan minimaliseren met CSP gaan we kijken hoe je het kan toepassen. Doorgaans wordt CSP toegepast via een request header met de key Content-Security-Policy
. Hier staan alle policies in vermeld. Een voorbeeld:
script-src self; style-src self;
Dit geeft aan dat alle scripts en styling enkel vanuit dezelfde domein van de web-applicate mogen worden ingeladen. Het is dus een simpele string waarbij iedere policy met een puntkomma is gescheiden.
Behalve via een header is het ook mogelijk om een CSP toe te passen via een meta tag maar dat is enkel aan te raden in uitzonderlijke gevallen waarbij bijvoorbeeld de maximale headergrootte wordt overschreden. In dit artikel gaan we daar daarom verder niet op in.
Hoe pas je het toe in jouw Laravel applicatie?
Een extra header kan je eenvoudig toevoegen binnen Laravel via een Middleware. Een dergelijke middleware zou er als volgt uit kunnen zien:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class AddContentSecurityPolicyHeaders
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
return $next($request)->withHeaders([
'Content-Security-Policy' => "script-src self; style-src self;",
]);
}
}
Vervolgens voeg je hem toe aan de gewenste middleware groep(en) in app.php
in de bootstrap folder:
use App\Http\Middleware\AddContentSecurityPolicyHeaders;
...
$middleware->web(append: [
...
AddContentSecurityPolicyHeaders::class,
...
]);
Je hebt nu CSP geïmplementeerd op al je web routes. De kans is alleen groot dat je applicatie niet meer goed laadt in je browser. Er zijn waarschijnlijk een hoop zaken die worden geblokkeerd (dit kan je zien in de console tab van je browser).
Dat komt omdat de directives (dat zijn script-src
en style-src
) slechts 1 expressie bevatten, namelijk self
. Hierdoor mogen externe en inline styling + scripts niet meer uitgevoerd worden. Je moet dus voor iedere beschikbare directive nagaan of ze voor jou van toepassing zijn in combinatie met de beschikbare expressies. Dat is vaak een irritant en tijdrovend klusje, met name omdat dat de errors naar aanleiding van je policies niet altijd even duidelijk zijn.
Gelukkig is er een package die je hierbij kan helpen.
Laravel CSP
Spatie heeft een mooie package gemaakt wat het toepassen en het beheer van CSPs een stuk eenvoudiger maakt. Zo biedt het:
- Presets, met presets kan je eenvoudig veel voorkomende policies toevoegen
- Eenvoudig toepassen van een nonce (een unieke token), om nog specifieker te bepalen welke scripts en styling toegepast mogen worden.
- Een centrale plek voor al het beheer van zaken mbt CSP.
Zo pas je Laravel CSP toe binnen je Laravel applicatie:
- Voeg de package toe:
composer require spatie/laravel-csp
- Voeg de CSP config toe aan je applicatie
php artisan vendor:publish --tag=csp-config
De config bevat een aantal handige opties maar de belangrijkste zijn presets en de nonce generator. Met presets kan je eenvoudig policy sets toevoegen.
'presets' => [
Spatie\Csp\Presets\Basic::class,
],
Standaard wordt de Basic
preset toegepast. Hiermee worden veel zaken al afgevangen, en is in de basis een prima startpunt. Het mooie van presets is dat je er meerdere kan combineren. Zo kan je custom presets maken maar ook andere bestaande presets eenvoudig toevoegen. Laravel CSP biedt een indrukwekkend lijstje aan presets voor populaire third party diensten:
- Voeg de CSP middleware toe
Voeg vervolgens deAddCspHeaders
middleware toe aan de middleware groepen waarop je CSP wil toepassen:
use Spatie\Csp\AddCspHeaders;
return Application::configure(basePath: dirname(__DIR__))
...
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
...
AddCspHeaders::class,
]);
})
...
)->create();
- Pas de nonce generator aan.
Standaard voegtlaravel-csp
een nonce-expressie toe aan descript-src
enstyle-src
directives. Een nonce is een reeks karakters, welke in dit geval gedurende een enkele request lifecycle geldig is.
0YP7cpBopUAtydHIkF4SwO7047w7COIh0i8VsvUj
De nonce wordt toegevoegd aan de CSP header en vervolgens dien je die toe te passen op alle assets die ingeladen moeten worden zoals CSS en Javascript.
Content-Security-Policy: nonce-ZP5LwyE9MMIpBxWp6K2UVAdMATLQV09IZ4InmK35
De browser checkt vervolgens of de nonce in de header overeenkomt met de nonce die in nonce-attribuut van de asset staat.
<link rel="prefetch" href="https://csp.example.test/build/assets/app-BFo9XHJS.js" nonce="ZP5LwyE9MMIpBxWp6K2UVAdMATLQV09IZ4InmK35" fetchpriority="low">
Is dit het geval? Dan wordt de asset ingeladen, zo niet dan blokkeert de browser dit. Goed om te weten: wanneer je de broncode van je pagina inspecteert zal het nonce attribuut altijd leeg zijn. Hierdoor kan het lijken alsof de nonce niet goed wordt toegepast. Dit is een beveiliging vanuit je browser. Op de achtergrond wordt hij dus wel degelijk toegepast.
Dankzij Laravel is het toepassen van een nonce op je assets erg eenvoudig, tenminste als je een recente versie van Laravel gebruikt. We gaan namelijk via de Vite facade een nonce genereren waardoor de nonce automatisch toegepast gaat worden op alle assets die Vite genereert.
Maak de volgende folder(s) aan in ./app/Support/Csp
. Een andere locatie is ook prima. Vervolgens maak je LaravelViteNonceGenerator.php
aan met de volgende inhoud:
<?php
namespace App\Support\Csp;
use Illuminate\Support\Facades\Vite;
use Spatie\Csp\Nonce\NonceGenerator;
class LaravelViteNonceGenerator implements NonceGenerator
{
public function generate(): string
{
return Vite::cspNonce();
}
}
In de csp config stel je de nonce_generator
in met de nieuwe generator:
'nonce_generator' => App\Support\Csp\LaravelViteNonceGenerator::class,
de cspNonce()
functie haalt de nonce op die via een andere functie in de volgende stap wordt gegenereerd.
- Pas de nonce toe op alle Vite assets
Nu is de configuratie van Laravel CSP voltooid, maar we moeten de applicatie dus nog instrueren om een nonce te genereren voor iedere request. Doorvoor kunnen we de methodeVite::useCspNonce()
gebruiken. Dit genereert een nonce en maakt de nonce beschikbaar in je gehele applicatie viaVite::cspNonce()
, zoals beschreven in de vorige stap. Daarnaast zorgt het ervoor dat de nonce als attribuut aan alle Vite gegenereerde assets wordt toegevoegd.
De juiste plek om Vite::useCspNonce()
aan te roepen is een aparte middleware. Hierdoor weet je zeker dat er bij iedere request een nieuwe nonce wordt gegenereerd. De middleware kan er als volgt uitzien:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class GenerateAndSetCspNonce
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (config('csp.nonce_enabled')) {
Vite::useCspNonce();
}
return $next($request);
}
}
Voeg vervolgens de middleware toe vóór alle andere middleware:
use Spatie\Csp\AddCspHeaders;
use App\Http\Middleware\GenerateAndSetCspNonce;
return Application::configure(basePath: dirname(__DIR__))
...
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
GenerateAndSetCspNonce::class,
...
AddCspHeaders::class,
]);
})
...
)->create();
Zo weet je zeker dat nonce aan het begin van de request lifecycle wordt gegenereerd en beschikbaar is in de rest van de cycle.
Bijvoorbeeld als je Vite::prefetch();
gebruikt, wat je doorgaans in de boot methode van je AppServiceProvider plaatst, moet je eerst Vite::useCspNonce();
hebben aangeroepen in combinatie met CSP. Als dat niet doet zullen alle prefetch links niet werken omdat een geldige nonce zal ontbreken.
Je hebt nu succesvol een redelijk solide CSP toegepast. Het kan echter zijn dat je applicatie nu niet goed meer laadt. Door de CSP kunnen styling en scripts worden geblokkeerd omdat ze nog niet voldoen aan je policies. Kijk daarom in de console tab van je browser. Je moet nu voor iedere melding gaan bekijken welke directive en expressie je nog mist.
Tip!
Gebruik je ontwikkeltools zoals horizon of telescope? Dan werken die waarschijnlijk niet meer door het toepassen van CSP. Aangezien dergelijke tools vooral lokale ontwikkeling zijn bedoeld zijn ze relatief veilig om te vertrouwen. Je kan er daarom voor kiezen om de routes van die diensten als een uitzondering te beschouwen. Zo kan je bijvoorbeeld je GenerateAndSetCspNonce
aanpassen:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class GenerateAndSetCspNonce
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (request()->is('telescope*', 'horizon*')) {
config(['csp.enabled' => false]);
return $next($request);
}
if (config('csp.nonce_enabled')) {
Vite::useCspNonce();
}
return $next($request);
}
}
In welke situatie pas je CSP toe?
Het is aan te raden om altijd CSP toe te passen voor applicaties die draaien in productie. Ook wanneer je lokaal aan het bouwen bent. Op die manier zie je namelijk gelijk de CSP errors in je console log en kan je ze verhelpen voordat het voor problemen zorgt in productie.
Ben je lekker aan het vibecoden met een nieuw idee of een lokale tool, dan kan het goed zijn dat CSP je meer in de weg zit dan dat het je helpt. In dat geval kan het beter later toegevoegd worden. Daarbij moet je wel in je achterhoofd houden dat achteraf CSP toepassen een stuk lastiger kan zijn dan dat je het aan het begin van de ontwikkeling toepast. De CSP errors zijn vaak wat cryptisch en onduidelijk, waarbij het niet altijd even helder is welke directive of policy je moet toevoegen of aanpassen.
Kijk ook naar de reporting mogelijkheden die Laravel CSP biedt. Dit kan je helpen om bewust te blijven van zaken die worden tegengehouden door je CSP in de browser van je bezoekers. Zo kan je gericht je CSP up to date houden en op de hoogte worden gebracht van eventuele aanvallen.
Tot slot
Je weet nu wat CSP inhoudt, hoe je het toepast binnen je Laravel applicatie. Bescherm dus je gebruikers, je eigen imago en die van je klanten en pas CSP toe op al je applicaties die in productie draaien.
Nog een belangrijk punt: pas je caching toe, dat direct via nginx verloopt bijvoorbeeld? Dan moet je daar apart nog CSP op instellen. Hetgeen beschreven in dit artikel past alleen CSP toe binnen je Laravel applicatie.