Reflected Cross-Site Scripting is a class of vulnerability that has been well-understood for over two decades, yet it continues to appear in production systems at a rate that suggests detection coverage — not developer awareness — is the primary gap. The reason for this is mechanical: the attack surface of a modern application has grown considerably faster than the tooling used to test it.
Most scanners approach XSS detection the way a spell-checker approaches grammar — they look for surface-level indicators in HTTP responses and flag strings that resemble known payloads. This works reasonably well for simple HTML-rendered endpoints, but it fails in the case we care about here: API responses that are consumed, transformed, and re-rendered by client-side JavaScript before any user ever sees them. By the time a vulnerable string reaches a DOM execution context, it may have passed through a JSON parser, a template engine, or a React reconciler. A pattern match on the raw HTTP response tells you very little about whether execution actually occurred.
This post describes the approach we took to build an automated scanner that addresses this gap. The core idea is straightforward: instead of matching patterns in text, render the injected response in a real browser and observe whether script execution follows. We describe how we structured the injection pipeline, how we handle the multiple contexts in which reflection can occur, and what we learned from running this against real API traffic.
Reflected XSS occurs when a server incorporates user-controlled input — a query parameter, a header value, a form field, a JSON body key — into its response without first neutralising HTML metacharacters or applying context-appropriate encoding. The injected content is not persisted; it travels in the request and returns in the response. The attack requires delivery, typically via a crafted URL sent to a victim, which means the impact depends on social engineering or an open redirect to bootstrap the payload. That said, the delivery barrier is low, and the consequence of a successful execution is high: the attacker runs arbitrary JavaScript in the victim’s authenticated session.
The vulnerability maps to CWE-79 (Improper Neutralization of Input During Web Page Generation) and to OWASP API Security Top 10 category A8, Security Misconfiguration. Its CVSS 3.1 base score under a typical configuration is
The combination of network-accessible, no authentication required, and high confidentiality and integrity impact places this firmly in the High severity tier.
The compliance exposure is also non-trivial. Unmitigated reflected XSS is a direct violation of PCI-DSS Requirement 6.2, which mandates protection of system components from known vulnerabilities. It conflicts with HIPAA 164.312(e)(1) for any application handling protected health information, and with GDPR Articles 25 and 32, which require data protection by design and appropriate technical controls for personal data. These are not edge-case mappings; they reflect the genuine risk of session-level data exfiltration that the vulnerability enables.
What motivated this work specifically was a pattern we observed repeatedly: traditional dynamic application security testing tools produce XSS findings for HTML-rendered endpoints but systematically miss API endpoints. The reasons are structural. A scanner that crawls hyperlinks and submits HTML forms will not naturally traverse REST endpoints protected by authentication tokens. A scanner that looks for payload reflection in text/html responses will not catch reflection in application/json responses that are later processed by a front-end framework. The API layer has become a significant blind spot.
The scanner is built around a single design principle: every finding must be confirmed by actual script execution in a browser, not inferred from textual analysis of a response body.
The input to the scanner is a JSON corpus of captured API requests, structured as a list of request objects each containing a URL, a headers list, and a body string. This format is compatible with the export format of standard proxies like Burp Suite and OWASP ZAP, which means the scanner can operate on real authenticated API traffic recorded during a normal user session rather than relying on crawling. For any API that sits behind an authentication layer — which is most APIs — this is the only practical approach.
For each request in the corpus, the scanner iterates over every injectable surface it can identify: each query parameter key, each header value, and the request body in multiple content-type encodings. Into each of these it substitutes a payload from a curated test library, fires the modified request, and evaluates the response in a headless Chromium instance via Selenium WebDriver. The evaluation is not a string search on the response text. It is a check for concrete execution indicators: a JavaScript alert dialog, or patterns in the live DOM that indicate script injection has taken place.
The scanner runs request workers in parallel using a ThreadPoolExecutor with up to five concurrent workers, which provides a reasonable balance between speed and stability when running against a remote target.
One of the more important observations that shaped the design is that reflection can occur in multiple independent contexts within the same request. A server that safely handles a user-supplied query parameter might still reflect a header value verbatim into an error message, or echo a JSON body field into a templated HTML response. Our scanner tests all three surface areas for every request.
URL query parameters are the most obvious injection point. The scanner parses the query string, iterates over each key, and constructs a modified URL with the payload substituted for the original value. Because multiple parameters may be present and each must be tested independently, a single URL can generate a significant number of test cases.
HTTP request headers are a frequently overlooked surface. Headers like User-Agent, Referer, X-Forwarded-For, and custom application headers are often logged, echoed in error responses, or reflected into rendered pages for debugging purposes. The scanner modifies one header at a time, sends the request with the injected header, and checks the response.
The request body is tested in three content-type encodings independently: application/x-www-form-urlencoded, application/json, and application/xml. This matters because a server may handle each content type differently — a JSON API endpoint that validates and encodes its JSON responses may still render an XML error message unsafely.
After the modified request is sent and the response is loaded in the headless browser, detection proceeds in two layers.
The primary layer is alert dialog detection. Selenium’s WebDriverWait polls for a JavaScript alert with a short timeout. If one is raised, the scanner accepts it, logs the finding, and moves on. This is the most reliable signal available — an alert dialog means the injected script executed unconditionally.
The secondary layer scans the live page source for execution indicators: the presence of patterns like alert(, eval(, innerHTML, onerror=, document.write(, and fetch( in positions that were not present before injection. This layer catches cases where a payload executes silently — for example a payload that exfiltrates cookies via a fetch() call rather than raising an alert — and also catches partial DOM injection where the payload was reflected but not yet executed.
Separately, and in parallel with the injection testing, the scanner performs a security header audit on each endpoint. It issues a normal GET request and inspects the Content-Security-Policy, X-Frame-Options, and X-XSS-Protection response headers. The absence of a CSP, or the presence of unsafe-inline or unsafe-eval directives within one, is flagged independently of whether a payload fired. The reasoning is that a missing or misconfigured CSP represents exploitability even for payloads the scanner did not discover.
The following diagram traces a single request through the full scanner pipeline from input to confirmed finding.
End-to-end scanner execution path for a single corpus request. Each injection branch runs concurrently. The detection phase uses execution-confirmed signals rather than textual pattern matching on the raw response.
The test payload library is designed to cover the range of contexts in which user input may appear in a server response, not just the obvious <script> tag injection. The same string that fires an alert in an HTML body context may be inert if reflected into a JavaScript string, a URL parameter, or a CSS attribute. The library is therefore organised by injection context and evasion tier rather than by payload family.
Table
When a payload triggers detection, the scanner exits the payload loop for that parameter and records the first confirming payload. It does not continue exhausting the library against an already-confirmed injection point, as the goal is detection rather than enumeration.
Running this scanner against API traffic from a range of application types surfaced several consistent patterns that are worth documenting.
Header reflection in error responses. A significant proportion of the header injection findings came not from the primary endpoint logic but from error handling paths. When a server receives a malformed or unexpected header value, it commonly includes the received value in a 400 Bad Request or 500 Internal Server Error response for diagnostic purposes. This reflection bypasses any input validation in the normal request processing path entirely, since the error handler is a separate code path that often lacks equivalent sanitisation.
JSON responses rendered as HTML. Several API endpoints returned Content-Type: application/json for normal requests but fell back to text/html error responses when given malformed input. The JSON response path was correctly sanitised; the HTML error path was not. A scanner that only tested the content-type: application/json response path would have missed these findings entirely.
CSP absent or misconfigured on the majority of tested endpoints. The security header audit found that most API endpoints in the tested corpus did not return a Content-Security-Policy header at all. Of those that did, a meaningful fraction included unsafe-inline in the script-src directive, which negates the protection that CSP is intended to provide. This is not a finding that produces a confirmed XSS execution, but it is a reliable indicator that a server is not hardened against the class of attack even when no injectable parameter was discovered.
DOM-based XSS at the boundary of server and client. In several cases, the server-side response was clean — the payload was correctly encoded before being inserted into the JSON response body — but the client-side JavaScript that consumed the response used innerHTML to render the decoded value. This is a DOM-based XSS pattern, not a reflected XSS pattern, but it was surfaced by the same headless browser rendering step that the scanner uses for reflected XSS detection. The distinction matters for remediation: the fix for reflected XSS is server-side encoding, but the fix for DOM-based XSS is replacing innerHTML with textContent or an equivalent safe API on the client side.
For each request in the corpus, the scanner tests six injection surfaces independently. A confirmed finding on one surface never suppresses testing on the others — a server that safely handles query parameters may still reflect a header value verbatim into an error response.
URL Parameters
The query string is parsed into individual key-value pairs. For each key, the scanner substitutes a payload for the original value, reconstructs the full URL, and navigates the browser to it. Only one parameter is modified at a time so that any confirmed finding can be attributed to a specific injection point.
HTTP Headers
Header values such as User-Agent, Referer, X-Forwarded-For, and custom application headers are frequently echoed verbatim into error messages or debug output — bypassing any input validation applied to the normal request path. The scanner replaces one header value per request and checks the response for payload reflection.
Request Body
The request body is tested across three content-type encodings independently: form-urlencoded, JSON, and XML. Each encoding may be routed through a separate server-side handler with its own sanitisation logic — a JSON endpoint that correctly encodes its responses may still render an XML error message unsafely.
DOM-Based XSS
The scanner checks for DOM-based XSS by injecting payloads directly into DOM sinks via JavaScript — including innerHTML, element attributes, location.hash, and dynamically created script tags. This catches cases where the server-side response is clean but client-side JavaScript unsafely renders the received data into the page.
Template Injection
For HTML page endpoints — identified by extensions such as .html, .php, .jsp, and .asp — the scanner injects payloads into the rendered page body to detect template rendering vulnerabilities. Non-HTML endpoints are skipped for this check to avoid false positives from non-rendering responses.
Form Fields
The scanner discovers all form elements present on each page, injects payloads into every input and textarea field, and submits the form via both GET and POST methods. This ensures coverage of server-rendered forms that may not appear explicitly in the captured API traffic corpus.
Detection Logic
After each injected response is loaded in the headless browser, detection runs in two layers. The primary layer is always evaluated first; the secondary is reached only when the primary does not fire.
Primary — Alert Dialog
The driver polls for a JavaScript alert() dialog after each navigation. An alert is the most reliable execution signal available — it confirms that the injected script ran in the browser’s JavaScript engine without ambiguity. When detected, the alert is dismissed and the finding is recorded immediately.
Secondary — DOM Execution Indicators
When no alert fires, the live page source is scanned for patterns indicating silent script injection — for example, a payload exfiltrating cookies via fetch() without calling alert(). A finding is raised only when both conditions are true: the payload is present in the page source, and at least one execution indicator also appears. Indicators include eval, fetch, innerHTML, onerror, onload, document.write, document.cookie, XMLHttpRequest, setTimeout, setInterval, and window.location.href.
The remediation for reflected XSS is well-established, but a few points are worth emphasising in the context of API endpoints specifically.
Sanitise and encode are two separate operations with different purposes. Sanitisation removes structure — strip HTML tags, reject characters that have no legitimate use in the expected input. Encoding transforms the remaining characters so they cannot be misinterpreted by the rendering context. Both are necessary. Sanitisation alone fails if the sanitiser has a bypass. Encoding alone fails if the encoding is not matched to the rendering context.
Apply a Content Security Policy and mean it. A CSP with script-src ‘self’ ‘strict-dynamic’ and without unsafe-inline or unsafe-eval will stop most reflected XSS payloads from executing even if the server-side encoding is incomplete. Treat CSP as a required second control, not an optional hardening measure. The absence of a CSP is a finding in its own right.
Do not treat API responses as inherently safe from XSS. A JSON response body that contains unencoded user input is not safe simply because the immediate consumer is a JavaScript parser rather than an HTML parser. The data will be used downstream — in a template, a DOM manipulation call, an error renderer — and the encoding requirements at that point cannot be predicted from the API layer. Encode at output, always, regardless of the response content type.
Avoid DOM sinks for user-controlled data. The client-side counterpart to server-side encoding is the avoidance of innerHTML, document.write(), eval(), setTimeout(string), and setInterval(string) for any data that originated outside the application. Use textContent for text, createElement() for structure, and setAttribute() for attributes. Most modern frameworks provide safe abstractions for all of these; use them rather than the underlying DOM APIs directly.
Integrate automated scanning into the deployment pipeline. The scanner described here is most useful when it runs continuously against a staging environment rather than as a periodic point-in-time assessment. API surfaces change with every deployment. A vulnerability introduced by a new endpoint or a modified error handler will not be caught by a scan that ran against the previous version of the application.
The patterns documented here are not novel. Reflected XSS in API endpoints has been reported in numerous CVE disclosures across the past several years, including in widely deployed enterprise software, identity providers, and developer tooling platforms. The consistent thread across these disclosures is not that the vulnerability is sophisticated — it is that it appeared in a part of the codebase that was not in scope for the security controls applied to the rest of the application.
In 2023 and 2024, reflected XSS vulnerabilities were disclosed in the API layers of multiple SaaS platforms, several of which carried CVSS scores in the High range. In several cases, the specific injection point was a search or filter parameter echoed into a server-rendered response, exactly the pattern the scanner described here is designed to detect. In each case, the vulnerability was exploitable without authentication, required only that a victim click a crafted link, and enabled complete session compromise for the duration of the victim’s authenticated session.
The supply chain dimension of this class of vulnerability is worth noting. When a reflected XSS exists in an API endpoint that is consumed by a widely deployed client library or embedded widget, a single crafted URL can be used to target users across every site that embeds the affected component. The exploitability is not bounded by the number of endpoints of the originating application.
Reflected XSS in API endpoints is detectable at scale, but only if the detection methodology matches the execution context. Pattern matching on raw HTTP responses is insufficient for the modern API layer, where user-controlled data travels through multiple transformations before it reaches a browser context. An execution-confirmed approach — rendering injected responses in a real browser and observing the result — is both more reliable and more precise.
The scanner described here ingests real captured API traffic, tests all injectable surfaces in parallel, and confirms findings through actual browser execution rather than textual inference. It additionally audits security headers as a parallel signal, because the absence of a well-configured CSP is itself an indicator of exploitability that holds independently of whether a specific payload was confirmed.
The underlying technique is straightforward. What makes it useful in practice is that it can be applied continuously and automatically to the full breadth of an application’s API surface, including authenticated endpoints, without requiring manual test case construction for each endpoint. Security controls applied only to the visible surface of an application leave the API layer exposed. Automated testing that covers the API layer as thoroughly as the front end is the baseline from which meaningful coverage begins.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.