4.2 Review Reflected XSS
4.2 - Review Reflected XSS
Reflected cross-site scripting appears when attacker-controlled input from the current request is echoed into HTML without encoding. Start from query parameters, form fields, and headers, then follow each value into the response body in a single request cycle.
What This Vulnerability Is
Reflected XSS is a client-side injection flaw. The application reads input from the current HTTP request and embeds it directly in the HTML response. If the value is not encoded for HTML, the browser may execute attacker-supplied script when a victim opens a crafted link.
Unlike stored XSS, the payload does not persist. The attacker typically delivers a malicious URL through phishing or ads. The victim's browser sends the parameter, and the server reflects it back unencoded. This maps to CWE-79 (Improper Neutralization of Input During Web Page Generation).
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | Search pages, login errors, greeting banners, 404 handlers, redirect messages, OAuth error pages |
| Input entry | Query strings, POST form fields, path segments, Referer echoes, cookie values shown in UI |
| Single-request echo | Value enters and exits in one round trip; no database between input and output |
| HTML sinks | f-strings, render_template_string, JSP scriptlets, @Html.Raw, Response.Write, SPA innerHTML |
| Weak controls | Regex input filters only, |safe / Markup(), missing auto-escape, JavaScript string concat |
| High impact views | Authenticated pages that reflect search terms, admin error handlers, password-reset flows |
Attack Payloads
Use these in authorized tests when request parameters are echoed in the immediate HTML response. Craft full URLs for phishing simulations; replace PAYLOAD with the value for the vulnerable parameter.
Pattern 1: Query parameter script injection
/login?error=<script>alert(document.domain)</script>
/reset?msg=<img src=x onerror=alert(1)>
/oauth/callback?state=<svg/onload=alert(1)>
Pattern 2: Attribute context breakout
?next="><script>alert(1)</script>
?return_url=javascript:alert(1)
?style=' onmouseover='alert(1)
Pattern 3: Path or fragment reflection
/404?uri=</title><script>alert(1)</script>
/help/<script>alert(1)</script>
Pattern 4: Header or cookie echo
Referer: https://evil.example/<script>alert(1)</script>
Cookie: locale=<img src=x onerror=alert(1)>
Pattern 5: Filter bypass variants
?error=<ScRiPt>alert(1)</ScRiPt>
?msg=<img src=x onerror=alert(1)>
?hint=<svg><script>alert(1)
Pattern 6: DOM-based follow-on (when reflection lands in JS)
?token=';alert(1)//
?nonce=</script><script>alert(1)</script>
?jsonp=alert(1)// (JSONP-style sinks)
Language-Specific Sinks and Dangerous APIs
Reflected XSS sinks appear wherever request data is written into HTML in the same handler. Trace each parameter to these APIs.
Python (Flask / Jinja2)
return f"<p class='error'>{request.args['error']}</p>"
return render_template_string(f"<div>{reset_msg}</div>")
return Markup(request.args.get("msg", ""))
Java (JSP / servlets)
Login failed: <%= request.getParameter("error") %>
<c:out value="${param.reason}" escapeXml="false"/>
out.println("Reset link sent to " + request.getParameter("email"));
C# (ASP.NET / Razor)
return Content($"<p>OAuth error: {Request.Query["error_description"]}</p>", "text/html");
@Html.Raw(Request.Query["msg"])
Response.Write(Request["failure_reason"]);
JavaScript (Node / Express)
res.send(`<p>Invalid token: ${req.query.token}</p>`);
document.title = location.hash.slice(1);
res.render("error", { detail: req.query.detail, autoEscape: false });
HTML (error pages and static responses)
<p>Password reset failed: <!-- reflected error param inserted without encoding --></p>
<meta http-equiv="refresh" content="0;url=REFLECTED_RETURN_URL">
Go
fmt.Fprintf(w, "<p>Login error: %s</p>", r.URL.Query().Get("error"))
template.HTML(reflectedMessage) // disables escaping
Sample Vulnerable Code in Python
from flask import Flask, request
app = Flask(__name__)
@app.route("/login")
def login_error():
# Attacker-controlled error message from failed login redirect
error = request.args.get("error", "")
# Sink: reflected value embedded in HTML — no encoding
return f"<h1>Sign in</h1><p class='alert'>{error}</p>"
Step-by-Step Review Walkthrough
- Find echo endpoints. Search for handlers that read request parameters and return HTML in the same action. Login failures, password-reset messages, and OAuth error pages are high yield.
- Trace the Python (or equivalent) read path. In the sample,
request.args.get("error")is attacker-controlled on every GET. Ask whether any validation runs before the response is built; input filtering is not a substitute for output encoding. - Locate the HTML sink. The f-string builds markup directly. Any
<script>inerrorruns in the victim browser when they open a crafted link. - Check error and validation branches. Failed logins and 404 handlers often echo user input in messages. Review every branch, not only the happy path.
- Inspect non-HTML contexts. Reflected values in
<script>blocks, event handlers,javascript:URLs, and JSONP callbacks need context-specific encoding, not only HTML body encoding. - Review URL decoding. Double-encoded payloads may bypass naive denylist filters applied before reflection.
- Confirm link-deliverable impact. Ask whether a crafted GET URL alone triggers execution. Reflected XSS often needs no POST or stored state.
Risk Impact Analysis
Session and account abuse. Script on a trusted origin can read non-HttpOnly cookies, perform actions as the victim, or steal anti-CSRF tokens.
Phishing at scale. Attackers distribute malicious links that execute in the victim's session context, making fake login overlays more convincing.
One-shot privilege paths. Reflected XSS on admin search or support tools can compromise operators who click attacker-supplied links.
Defense bypass. Reflected payloads may chain with open redirects or CSRF gaps when script runs in an authenticated session.
Vulnerable Examples in Other Languages
Java
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String reason = request.getParameter("reason");
request.setAttribute("failureReason", reason);
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
// login.jsp: <p class="error"><%= request.getAttribute("failureReason") %></p>
C
public IActionResult ResetPassword(string token, string msg)
{
ViewBag.StatusMessage = $"Reset failed: {msg}";
return View();
}
// ResetPassword.cshtml: @Html.Raw(ViewBag.StatusMessage)
JavaScript
// Reflected OAuth error parameter written into the DOM
const err = new URLSearchParams(location.search).get("error_description") || "";
document.getElementById("oauth-error").innerHTML = `Authorization failed: ${err}`;
HTML
<!-- JSP echoes password-reset message without encoding -->
<p><%= request.getParameter("msg") %></p>
<!-- 404 handler reflects requested URI -->
<div class="not-found">Page not found: <%= request.getAttribute("uri") %></div>
Go
func loginError(w http.ResponseWriter, r *http.Request) {
errMsg := r.URL.Query().Get("error")
tmpl := `<html><body><h1>Login</h1><p class="alert">{{.Error}}</p></body></html>`
t := template.Must(template.New("login").Parse(tmpl))
t.Execute(w, map[string]string{"Error": errMsg}) // text/template, not html/template
}
Fix: Safer Patterns and Libraries to Use
Python
Use templates with auto-escaping enabled. Never pass reflected input through Markup() or |safe.
from flask import Flask, render_template, request
from markupsafe import escape
app = Flask(__name__)
@app.route("/login")
def login_error():
error = request.args.get("error", "")
return render_template("login.html", error=error)
# Manual encoding when building non-template fragments:
safe_error = escape(error)
Important: render_template_string with user-influenced template text is both reflected XSS and SSTI risk. Use static template files and pass data as variables.
# Validate format when the parameter has a fixed shape (defense in depth):
import re
if not re.fullmatch(r"[a-zA-Z0-9\s.,!?-]{1,128}", error):
error = ""
Java
Encode at the HTML sink. Prefer JSTL or OWASP Encoder over regex-only input filters.
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<p>Login failed: <c:out value="${failureReason}" /></p>
import org.owasp.encoder.Encode;
String safe = Encode.forHtml(request.getParameter("error"));
model.addAttribute("safeError", safe);
Important: Thymeleaf th:utext and unescaped JSP scriptlets bypass default protections. Use th:text for reflected request data.
C
Razor encodes by default. Avoid Html.Raw on request-derived strings.
@* Safe default encoding *@
<p class="alert">@Model.ErrorMessage</p>
using System.Net;
var encoded = WebUtility.HtmlEncode(msg);
ViewBag.SafeMessage = $"Reset failed: {encoded}";
For controlled HTML subsets in reflected content, use a maintained sanitizer with an explicit policy.
Go
Use html/template, not text/template, for HTML responses.
import "html/template"
var loginTmpl = template.Must(template.New("login").Parse(
`<h1>Sign in</h1><p class="alert">{{.Error}}</p>`))
func loginError(w http.ResponseWriter, r *http.Request) {
errMsg := r.URL.Query().Get("error")
loginTmpl.Execute(w, struct{ Error string }{Error: errMsg})
}
Important: html.EscapeString helps only when you must build strings manually; templates apply context-aware rules automatically.
Verify During Review
- Every reflected parameter is HTML-encoded at the template or response sink, including error and validation messages.
- Search, login failure, and 404 handlers do not echo raw request data.
- JavaScript contexts that embed reflected values use JSON serialization plus encoding, not string concatenation.
- Redirect and callback parameters are validated; open redirects are not chained with reflected script sinks.
- Encoding is applied per output context (HTML body, attribute, URL, JavaScript).
- CSP and HttpOnly cookies are defense in depth, not the primary XSS control.