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.
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.
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.
# 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.
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.
# 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
| Context | Injection structure | Confirming artifact | Severity |
|---|---|---|---|
| HTML body | <img src=x onerror=document.__ap=62615533> | DOM property set on render (onerror fires without interaction) | Critical |
| HTML attribute | Quote-break + event handler (onmouseover, onfocus) | Property set after synthetic trigger event | High; Critical if self-firing sink available |
| JavaScript string | String termination + nonce assignment + comment suffix | Property set on synchronous parse - no trigger needed | Critical |
| URL / href scheme | javascript:void(document.__ap=62615533) | Navigation event + DOM property after synthetic click | High; 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: " 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.
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.
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: " 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.