Engineering · Aug 17, 2026 · 7 min read

Reflected XSS: why the output context decides both the payload and the severity

A reflected value in an HTML body, an attribute, a JavaScript string, and a href-scheme sink each needs a different payload to confirm execution - and a scanner keyed on one context silently misses the others.

Reflected XSS is not one vulnerability class - it is four injection contexts that share a name. A value reflected into raw HTML, an HTML attribute, a JavaScript string literal, and a URL-scheme sink each requires a structurally different payload to reach execution. A scanner keyed only on the HTML-body context silently misses attribute injection, JavaScript-string termination, and javascript:-URI sinks - and those gaps are where most findings live in modern SPAs.

01Four contexts, four structurally different injections

The output context is determined by what the application does with the reflected value after the template engine writes it to the response. HTML body reflection allows tag injection directly. Attribute reflection requires breaking out of the enclosing quote before a new event handler can be introduced. JavaScript string reflection requires terminating the string literal and escaping the script block. URL-scheme reflection into an href or src attribute opens a javascript: URI path that many output-encoding rules forget to block.

HTML body ──► <div>{{value}}</div> inject <script> or <img onerror> Attribute ──► <input value="{{value}}"> break quote, add event handler JS string ──► var x = "{{value}}"; terminate string, inject code URL / href ──► <a href="{{value}}"> inject javascript: URI scheme
Four contexts, four distinct injection shapes. An HTML-body payload fails structurally in a JS string - and vice versa. A scanner keyed on one misses the other three entirely.

02The confirming oracle: execution, not reflection

Reflection is reachability, not execution. Seeing the injected bytes appear in the response body proves the input survived the round trip - it does not prove JavaScript ran in a browser context. The confirming artifact is a measurable DOM side-effect produced only if the injected payload executed: a specific DOM property set to a benign nonce, a unique element inserted into the document, or a navigation event from a javascript: URI that contains the same nonce. The oracle value used is 62615533 - if that literal appears as the result of a headless-render DOM property check, the script ran. No real credential is accessed, no resource is written, and the nonce is meaningless outside the probe session.

xss-probe.httphttp
# context: value reflected inside an HTML attribute
# baseline - value appears inside the attribute, no structural break
GET /search?q=hello
HTTP/1.1 200  <input value="hello">

# probe - close the attribute, inject onerror on an img tag
GET /search?q="%20onmouseover%3Ddocument.__ap%3D62615533%20x%3D"
HTTP/1.1 200  <input value="" onmouseover=document.__ap=62615533 x="">

# headless render with synthetic hover event
document.__ap ──► 62615533   # execution confirmed

The differential that matters is between the baseline response (attribute value, no break) and the probe response (attribute structurally broken, event handler injected). Only if the headless browser confirms the property is set after the synthetic trigger is the finding raised as confirmed. A response that merely reflects the percent-encoded string as text is reachability, not execution.

Proven - HighReflected XSS - HTML attribute context - /search?q=
GET /search?q=%22%20onmouseover%3Ddocument.__ap%3D62615533%20x%3D%22 ──► <input value="" onmouseover=document.__ap=62615533 x=""> Headless render + synthetic hover: document.__ap === 62615533 Baseline GET /search?q=hello returns value inside attribute - no break. Differential confirmed across 3 repeats.
Oracle: headless DOM property check after synthetic user event. No credentials accessed, no writes made. Rated high - interaction required (onmouseover). Escalates to critical if a self-firing sink (onerror, onload) can be substituted.

03Context-by-context probe strategy

Each context requires a different probe structure, a different injection payload, and a different confirming artifact. Sending an HTML-body tag into a JavaScript-string sink produces a parse error, not execution - and a parse error is not confirmation. The right probe sequence is: identify the output location in the rendered response first, then craft the minimum structural injection that would reach a callable sink if the output encoding is absent or incomplete.

js-string-probe.httphttp
# context: value interpolated into a JS string literal
# response fragment: <script>var query = "{{value}}";</script>

# probe: terminate string, write nonce, comment-out the remainder
GET /results?q=";document.__ap=62615533;//
HTTP/1.1 200
<script>var query = "";document.__ap=62615533;//";</script>

Headless render  ──► document.__ap === 62615533  # no user interaction - critical
ContextInjection structureConfirming artifactSeverity
HTML body<img src=x onerror=document.__ap=62615533>DOM property set on render (onerror fires without interaction)Critical
HTML attributeQuote-break + event handler (onmouseover, onfocus)Property set after synthetic trigger eventHigh; Critical if self-firing sink available
JavaScript stringString termination + nonce assignment + comment suffixProperty set on synchronous parse - no trigger neededCritical
URL / href schemejavascript:void(document.__ap=62615533)Navigation event + DOM property after synthetic clickHigh; Critical if link can be auto-followed

One additional failure mode is context blending. A framework that HTML-encodes the value before interpolating it into a JavaScript block stops HTML-body injection but may not stop JS-string termination: &quot; in HTML context is harmless, but the character it represents - a double-quote - is exactly what terminates a JS string. Encoding for the wrong context is not defense; it is noise that gives false confidence. Per-context encoding must be applied at the sink, not at a shared sanitization layer upstream.

04Severity follows the confirmed execution path

Severity is set by the confirmed execution context, not by the category label. A JavaScript-string termination that executes synchronously on parse is always critical - no user interaction is required and execution is immediate. An HTML-body injection that reaches an onerror sink on a tag the browser loads automatically is also critical. An attribute injection requiring a hover or a click is high. A javascript: URI in an href that demands a deliberate navigation is high with a clear critical escalation path if the link is auto-followed. Rating all four contexts at a flat "reflected XSS, medium" loses the distinction that separates a one-sprint cleanup item from an incident-grade finding on a login or account page.

4
output contexts, four distinct probes
0
credentials accessed or resources written
1
DOM oracle per confirmed execution path

The fix is context-aware encoding applied at the sink. HTML-encode for body and attribute injection, JavaScript-encode string literals independently of any HTML layer, and validate against an explicit URL-scheme allowlist for href and src attributes. A single HTML-entity encoder applied to a JavaScript string context provides no protection: &quot; encodes to " in HTML, but the raw double-quote that terminates a JS string is still present in the script context after HTML parsing. The context that the probe confirmed vulnerable is the context where the targeted fix applies.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →