Skip to content

Reflected XSS

The application takes a value from the request - a URL parameter, form field, or header - and echoes it back into the immediate response without context-aware encoding. The payload lives in the request, so there is nothing stored: each victim has to be sent a crafted URL and click it. One request in, one response out, your code runs in their session.

# Unique canary per input - greppable, no special chars to get filtered
http://<TARGET>/search?q=xss7a9c
# Find the canary in the response, read the surrounding markup, then pick the payload
# body context → <svg onload=alert(document.domain)>
# attribute context → "><svg onload=alert(document.domain)>
# JS string context → ';alert(document.domain)//

Success indicator: alert(document.domain) fires when you load the crafted URL. document.domain (not 1) proves which origin executed it - important when reflections bounce through redirects or iframes.

Reflected XSS fires only for someone who sends your exact request, so impact depends entirely on delivery:

  • The payload rides in a URL (GET) or a form you make the victim submit (POST). GET is the common case because a link is easy to deliver.
  • Delivery is a social-engineering step - a link in an email, chat, or another site. That makes reflected XSS per-click and noisier to exploit than stored, which fires for every viewer with no link.
  • It is still serious: one click from a logged-in admin is enough. The ceiling is the same as any XSS once code runs (see the cluster overview capability table).

Send a unique, filter-neutral marker through each candidate and grep the response for it.

  1. Enumerate inputs: every URL parameter, every form field, and reflected headers (Referer, User-Agent, X-Forwarded-For, sometimes Cookie).
  2. Send a distinct canary per input - e.g. xss7a9c, xss7a9d - so you know which input produced which reflection.
  3. Search the response body for each canary. No reflection means no reflected XSS on that input (it may still be stored or DOM-based).
  4. For every reflection, note where in the markup it landed - that is the context, and it decides the payload.

Plain alphanumeric canaries avoid the trap of a payload being filtered before you have even confirmed the input reflects.

The canary’s surrounding markup tells you which payload class applies. This is the single most important step - a reflected payload that “doesn’t fire” is usually right input, wrong context, not a WAF.

Canary lands in Example Payload class
HTML body <p>Results for xss7a9c</p> Open a tag: <svg onload=…>
Tag attribute <input value="xss7a9c"> Break out: "><svg onload=…> or add a handler: " onfocus=… autofocus x="
JS string var q = "xss7a9c"; Break the string: ";alert(document.domain)//
URL attribute <a href="xss7a9c"> javascript:alert(document.domain)
CSS <style>…xss7a9c…</style> CSS-injection; rarely direct JS in modern browsers

See Payload construction for the full per-context catalog, and Filter bypasses when the obvious payload is stripped or encoded.

Step 3 - Check the reflection’s encoding

Section titled “Step 3 - Check the reflection’s encoding”

The same input often reflects in more than one place, encoded differently in each. A value HTML-entity-encoded in the visible body may be reflected raw inside a hidden attribute or a JS variable. Test every reflection, not just the first:

# One input, three reflections - only the third is exploitable
<p>Results for: &lt;svg&gt;</p> ← entity-encoded, dead
<input value="&lt;svg&gt;"> ← entity-encoded, dead
<script>var q="</script>...";</script> ← raw in JS string, LIVE

Once a payload fires:

  1. Replace the alert proof with the real action - usually session exfiltration. The cookie-logger pattern and callback infrastructure live in XSS to session.
  2. If cookies are HttpOnly, pivot to an in-browser CSRF action or scrape the DOM for data and tokens - see XSS+CSRF chain.
  3. Build the final deliverable: the crafted URL (or auto-submitting form page) you would actually send a victim.
  • Reflection is HTML-entity-encoded everywhere. The app is output-encoding correctly in this sink. Check other sinks/contexts for the same input before concluding it is safe.
  • Fires in Repeater but not the browser. The browser may re-encode the URL, or a client-side framework may re-render and strip it. Test in a real browser, and consider whether it is actually DOM-based.
  • Payload works logged-out but the target is logged-in-only. Make sure your proof runs in the authenticated view; some reflections only exist on authenticated pages.
Step Action
Map Every param, field, and reflected header
Canary Unique alphanumeric marker per input
Context Read the markup around each reflection
Payload Pick by context (catalog)
Bypass If filtered, see Filter bypasses
Confirm alert(document.domain)
Escalate Session theft / CSRF chain
  • Reflection in HTML body, no filtering → <svg onload=…>; escalate straight to session theft
  • Reflection inside an attribute → break out with "> or add autofocus+onfocus; see Payload construction
  • Reflection inside a JS string → ';alert(document.domain)//, then repair the string so the page doesn’t break
  • Payload stripped or encoded → Filter bypasses
  • No reflection anywhere → the sink may be client-side (DOM-based) or persisted and rendered elsewhere (Stored / Blind)
  • Confirmed but cookies are HttpOnly → XSS+CSRF chain or DOM scraping