XSS Preventie
Geen Scriptcarnaval Vandaag
Webrisico is zelden mysterieus. Het zit meestal in voorspelbare fouten die onder tijdsdruk blijven staan.
Voor XSS Preventie ligt de winst in context-gebonden output, browserbeperkingen en veilige frontend-baselines.
Dat maakt security minder een losse controle achteraf en meer een standaardkwaliteit van je product.
Directe maatregelen (15 minuten)
Waarom dit telt
De kern van XSS Preventie is risicoreductie in de praktijk. Technische context ondersteunt de maatregelkeuze, maar implementatie en borging staan centraal.
Verdediging: hoe het wel moet
Het mooie aan de webbeveiliging-industrie is dat we al twintig jaar oplossingen hebben voor XSS, en dat we ze collectief weigeren te gebruiken. We weten hoe het moet. We hebben de tools. We hebben de standaarden. We hebben de documentatie. En toch, jaar na jaar, verschijnt XSS in de OWASP Top 10.
Het is alsof de hele mensheid een handleiding heeft voor het voorkomen van branden, maar we blijven collectief onze sigaretten in het droge gras gooien.
Maar goed, voor de mensen die daadwerkelijk hun werk willen doen, hier zijn de verdedigingen.
Output Encoding – de eerste verdedigingslinie
De meest fundamentele verdediging tegen XSS is output encoding: het onschadelijk maken van speciale tekens voordat ze in de HTML terechtkomen.
< wordt <
> wordt >
" wordt "
' wordt '
& wordt &
Context-specifieke encoding is essentieel. HTML-encoding beschermt in de HTML-context maar niet in JavaScript-strings. JavaScript-encoding beschermt in JavaScript maar niet in HTML-attributen. URL-encoding beschermt in URL’s maar niet in HTML.
# Python/Flask -- veilig (Jinja2 auto-escapes)
{{ user_input }}
# ONVEILIG -- | safe schakelt auto-escaping uit
{{ user_input | safe }}// JavaScript -- ONVEILIG
element.innerHTML = userInput;
// JavaScript -- veilig
element.textContent = userInput;De regel is simpel: gebruik textContent in plaats van
innerHTML wanneer je tekst rendert. Gebruik de
auto-escaping van je template engine. En als je ooit | safe
of {!! !!} of dangerouslySetInnerHTML typt,
stop dan even en vraag jezelf af waarom.
Content Security Policy (CSP) – de tweede verdedigingslinie
Content Security Policy is een HTTP-header die de browser vertelt welke bronnen een pagina mag laden. Het is de meest krachtige verdediging tegen XSS, en tegelijkertijd de meest onderbenutte.
Content-Security-Policy: default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Met deze header kan de pagina alleen scripts laden van zijn eigen
domein ('self'). Inline scripts
(<script>alert(1)</script>) worden geblokkeerd.
Event handler attributen (onerror=alert(1)) worden
geblokkeerd. eval() wordt geblokkeerd.
Belangrijke CSP directives voor XSS-preventie:
| Directive | Functie |
|---|---|
script-src |
Welke bronnen mogen scripts leveren |
style-src |
Welke bronnen mogen stylesheets leveren |
default-src |
Fallback voor alle resource types |
frame-ancestors |
Wie mag deze pagina in een iframe laden |
base-uri |
Welke base URIs zijn toegestaan |
form-action |
Waar mogen formulieren naartoe submitten |
Nonce-based CSP:
Content-Security-Policy: script-src 'nonce-R4nd0mStr1ng';
<!-- Dit script wordt uitgevoerd (juiste nonce) -->
<script nonce="R4nd0mStr1ng">
// Legitieme applicatie code
</script>
<!-- Dit script wordt geblokkeerd (geen nonce) -->
<script>alert('XSS')</script>Met nonce-based CSP moeten alle scripts een unieke, per-request gegenereerde nonce hebben. Een aanvaller die XSS injecteert, kent de nonce niet en kan dus geen scripts uitvoeren.
Strict-dynamic:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-R4nd0m';
Met strict-dynamic mogen scripts die via een vertrouwd
script geladen worden (met de juiste nonce), zelf ook weer scripts
laden. Dit maakt het makkelijker om CSP te implementeren in complexe
applicaties met veel dynamisch geladen scripts.
HttpOnly Cookies
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
De HttpOnly vlag voorkomt dat JavaScript de cookie kan
lezen via document.cookie. Dit beschermt specifiek tegen de
meest directe vorm van XSS-exploitatie: cookie theft.
Maar – en dit is belangrijk – HttpOnly beschermt niet
tegen:
- CSRF via XSS (de browser stuurt cookies automatisch mee)
- Keylogging
- DOM-manipulatie
- Phishing via pagina-vervanging
- localStorage theft
HttpOnly is een slot op een van de deuren. Beter dan
niks, maar geen volledige oplossing.
DOMPurify – sanitization voor de client-side
// ONVEILIG
element.innerHTML = userInput;
// VEILIG met DOMPurify
element.innerHTML = DOMPurify.sanitize(userInput);DOMPurify is een JavaScript-library die HTML sanitizetert door alle potentieel gevaarlijke elementen en attributen te verwijderen. Het is de industriestandaard voor client-side HTML-sanitization.
// DOMPurify configuratie voorbeelden
DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});Specifieke verdedigingen per XSS-type
| XSS Type | Primaire verdediging | Secundaire verdediging |
|---|---|---|
| Reflected | Output encoding | CSP, input validatie |
| Stored | Output encoding | CSP, input sanitization |
| DOM-based | Gebruik textContent |
DOMPurify, CSP |
Voor DOM-based XSS specifiek:
- Vermijd
innerHTML,document.write(),eval() - Gebruik
textContentofsetAttribute()in plaats vaninnerHTML - Valideer de
originbijpostMessageevents:
window.addEventListener('message', function(event) {
// ALTIJD de origin controleren!
if (event.origin !== 'https://vertrouwd-domein.nl') {
return; // Negeer berichten van onbekende bronnen
}
// Gebruik textContent, NIET innerHTML
document.getElementById('notificatie').textContent = event.data;
});De cynische waarheid over “het is maar JavaScript”
Er zit een bepaald type manager in de tech-industrie – je kent ze wel, die figuren met hun poloshirts en hun “agile” buzzwords – die bij elke pentest- rapportage met een XSS-bevinding zeggen: “Maar het is maar een pop-up. Hoe erg kan het zijn?”
Het is maar een pop-up. Precies. En een kernreactor is maar een waterkoker. En een pistool is maar een buisje met een veertje. De vereenvoudiging mist het punt zo spectaculair dat het bijna bewonderenswaardig is.
Het is maar JavaScript. Datzelfde JavaScript dat:
- Je sessie kan overnemen
- Je wachtwoorden kan loggen
- Je scherm kan vervangen met een phishing-pagina
- Transacties kan uitvoeren namens jou
- Je bedrijfsgegevens kan exfiltreren
- Een persistent backdoor kan installeren in je browser
“Het is maar JavaScript” is het beveiligingsequivalent van “het is maar een beetje water” terwijl de dam breekt.
Het probleem is niet dat managers niet begrijpen wat JavaScript kan. Het probleem is dat ze niet willen begrijpen wat JavaScript kan. Want als ze het zouden begrijpen, zouden ze ook moeten begrijpen dat hun applicatie al jaren kwetsbaar is, dat ze al jaren de verkeerde keuzes hebben gemaakt, en dat het fixen geld kost. En het is altijd makkelijker om het risico weg te wuiven dan om het aan te pakken.
Daarom maken we die mooie alert(1) pop-ups. Niet omdat
dat het enige is wat we kunnen, maar omdat het de simpelste manier is om
te bewijzen dat er code- executie mogelijk is. De echte exploit komt
erna. En die laat je zien in een gecontroleerde omgeving, met de juiste
tooling, zodat ook de meest cynische manager het risico niet meer kan
ontkennen.
Prototype Pollution verdediging
Voor prototype pollution gelden specifieke verdedigingsmaatregelen:
1. Gebruik Object.create(null) voor
dictionaries:
// ONVEILIG -- erft van Object.prototype
let config = {};
// VEILIG -- geen prototype
let config = Object.create(null);2. Gebruik Map in plaats van gewone
objecten:
// ONVEILIG
let cache = {};
cache[userInput] = value;
// VEILIG
let cache = new Map();
cache.set(userInput, value);3. Controleer met hasOwnProperty:
function safeMerge(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (key === '__proto__' || key === 'constructor') {
continue; // Skip gevaarlijke keys
}
target[key] = source[key];
}
}
return target;
}4. Update je dependencies:
lodash>= 4.6.2- Vermijd
deep-extend,hoistjs, en andere bekende kwetsbare libraries
5. Freeze het prototype:
Dit is de nucleaire optie: niemand kan meer eigenschappen toevoegen
aan Object.prototype. Het breekt mogelijk legitieme code
die prototypes uitbreidt, maar het elimineert prototype pollution
volledig.
Node.js Injection verdediging
1. Gebruik nooit eval() op user
input:
// NOOIT DIT DOEN
app.get('/calc', (req, res) => {
let result = eval(req.query.expr); // RCE!
res.send(String(result));
});
// VEILIG alternatief -- gebruik een parser
const mathjs = require('mathjs');
app.get('/calc', (req, res) => {
let result = mathjs.evaluate(req.query.expr);
res.send(String(result));
});2. JSON.parse() is veilig, eval()
op JSON is dat niet:
3. Gebruik --frozen-intrinsics
flag:
Dit bevriest alle ingebouwde objecten, waardoor prototype pollution onmogelijk wordt.
4. Vermijd vm als sandbox:
De vm module van Node.js is geen beveiligings-sandbox.
Gebruik vm2 of isolated-vm als je echt code in
een sandbox wilt uitvoeren, maar weet dat ook deze libraries
kwetsbaarheden kunnen hebben.
Samenvatting: de XSS gereedschapskist
We hebben in dit hoofdstuk een reis gemaakt van de simpelste
<script>alert(1) </script> tot een volledig
operationeel XSS command and control systeem. Laten we de belangrijkste
lessen op een rij zetten.
XSS Typen
| Type | Payload locatie | Server betrokken | Vereist klik |
|---|---|---|---|
| Reflected | URL parameters | Ja | Ja |
| Stored | Database/bestand | Ja | Nee |
| DOM-based | URL fragment/DOM | Nee | Soms |
Verdedigingsoverzicht
| Maatregel | Beschermt tegen | Effectiviteit |
|---|---|---|
| Output encoding | Reflected, Stored XSS | Hoog |
| CSP (nonce-based) | Alle XSS-typen | Zeer hoog |
| HttpOnly cookies | Cookie theft via XSS | Medium |
| DOMPurify | DOM-based XSS | Hoog |
| textContent | DOM-based XSS | Hoog |
| Object.create(null) | Prototype pollution | Hoog |
| JSON.parse | Node.js injection (vs eval) | Hoog |
Verder lezen en referenties
| Bron | Onderwerp |
|---|---|
| OWASP XSS Prevention Cheat Sheet | Output encoding per context |
| OWASP Testing Guide - XSS | Testmethodologie |
| PortSwigger Web Security Academy - XSS | Interactieve labs |
| Google CSP Evaluator | CSP-beleid testen |
| DOMPurify GitHub repository | Client-side sanitization |
| CWE-79: Improper Neutralization of Input | Formele kwetsbaarheidsdefinitie |
| CWE-1321: Improperly Controlled Modification | Prototype pollution CWE |
| NodeGoat OWASP Project | Node.js beveiligingslabs |
| HackTricks - XSS | Uitgebreide payload collectie |
| Browser security test suite | Browserbeveiliging valideren |
De volgende keer dat iemand je vertelt dat XSS “maar een pop-up” is, nodig ze dan uit voor een demonstratie in een gecontroleerde lab-omgeving. Niets overtuigt zo snel als het zien van je eigen toetsaanslagen op het scherm van iemand anders.
In het volgende hoofdstuk duiken we in Server-Side Request Forgery (SSRF) – een kwetsbaarheid waarbij we de server zelf als proxy gebruiken om plaatsen te bereiken waar we niet horen te zijn. Als XSS gaat over het manipuleren van de browser, gaat SSRF over het manipuleren van de server. En servers hebben doorgaans toegang tot veel interessantere dingen dan browsers.
Verder lezen in de kennisbank
Deze artikelen in het portaal geven je meer achtergrond en praktische context:
- API's — de onzichtbare lijm van het internet
- SSL/TLS — waarom dat slotje in je browser ertoe doet
- Encryptie — de kunst van het onleesbaar maken
- Wachtwoord-hashing — hoe websites je wachtwoord opslaan
- Penetratietesten vs. vulnerability scans
Je hebt een account nodig om de kennisbank te openen. Inloggen of registreren.
Gerelateerde securitymaatregelen
Deze artikelen bieden aanvullende context en verdieping: