jan-karel.nl

XSS Preventie

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  &lt;
>  wordt  &gt;
"  wordt  &quot;
'  wordt  &#x27;
&  wordt  &amp;

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 textContent of setAttribute() in plaats van innerHTML
  • Valideer de origin bij postMessage events:
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:

Object.freeze(Object.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:

// VEILIG
let data = JSON.parse(userInput);

// ONVEILIG
let data = eval('(' + userInput + ')');

3. Gebruik --frozen-intrinsics flag:

node --frozen-intrinsics app.js

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.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Webbeveiliging ← Home