VNX-JAVA-030 – Java Reflected Cross-Site Scripting
Overview
This rule detects Java code that sends HTTP request data back to the browser inside an HTML response without encoding it. The value can come straight from request.getParameter(), getHeader(), getCookies(), getQueryString(), getPathInfo() or getRequestURI(), from an annotated Spring or JAX-RS handler parameter (@RequestParam, @PathVariable, @RequestBody), or from a local assigned from one of those. An attacker who gets a victim to follow a crafted link makes the page run the attacker’s script in the victim’s session. This is reflected cross-site scripting, CWE-79.
Severity: High | CWE: CWE-79 – Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’)
Why This Matters
Script that runs in the origin of your application can read anything the user can read and do anything the user can do: it can steal session tokens that are not HttpOnly, submit forms, change the account email, or rewrite the page to harvest credentials. A reflected value needs no stored data, only a link, so it spreads by email, chat and search results.
Output encoding is the control that works, because it is applied at the one place the context is known. Input validation helps but cannot stand in for it: a name such as O'Brien is valid input and still breaks out of a single-quoted attribute.
What Gets Flagged
The rule reports the response write whose argument carries request data:
response.getWriter()followed byprint,println,write,printf,formatorappend, including a call split across lines;- the same methods on a local assigned from
getWriter()orgetOutputStream(), and on aPrintWriter,JspWriterorServletOutputStreamparameter; response.getOutputStream().write(...);- a JSP scriptlet expression
<%= ... %>andout.print/out.printlnin a.jspfile; - a
returnof a Spring@ResponseBodyor@RestControllermethod, or aResponseEntitybody, whose mapping producestext/html(produces = "text/html",MediaType.TEXT_HTML).
The value is followed up to ten assignments back inside the enclosing method (at most 120 lines up), through lists, maps and builders, through a for-each over request.getCookies(), and across statements that span several lines.
// FLAGGED: request value concatenated into the page
String name = request.getParameter("name");
response.getWriter().println("<p>Hello " + name + "</p>");
// FLAGGED: through a writer local and a cookie
PrintWriter out = response.getWriter();
for (Cookie c : request.getCookies()) {
if (c.getName().equals("theme")) theme = c.getValue();
}
out.print("<body class='" + theme + "'>");
// FLAGGED: the list element is the header value
items.add("bread");
items.add(request.getHeader("X-Extra"));
String last = items.get(1);
response.getWriter().println("<li>" + last + "</li>");
// FLAGGED: Spring method that produces HTML
@GetMapping(value = "/greet", produces = "text/html")
@ResponseBody
public String greet(@RequestParam String name) {
return "<h1>Hello " + name + "</h1>";
}
<%-- FLAGGED --%>
<p>You searched for: <%= request.getParameter("q") %></p>
Helpers in other files
A call into another class of the project is read from that class’s own source instead of being guessed at. For obj.m(args), Cls.m(args) and new Cls(args).m(args) the class is found by the declared type of obj (a local, a parameter, or the new Cls(...) it was assigned from) or by the static class name, and the method (at most 60 lines, overloads told apart by arity) is summarised as one of:
- constant: every return is a literal, an
UPPER_CASEname, a field initialised to a literal and never assigned again, or the result of a constant method of the same class. The call is as constant as that literal, whatever it is handed. - a function of its arguments: every return is constant once the parameters, and the fields the class’s one constructor stores from its own parameters, are taken as constants. The call is only as risky as the arguments it depends on: a getter of a value stored by
new Cls(arg)is as tainted asarg, and a method that ignores one of its parameters is not tainted by it. - opaque: anything else, such as a method that reads a request value or a header, a field a setter can change, or a call the summary cannot see through.
A value of an interface or base class type runs the method of every project class that extends or implements it, so one implementation that returns its parameter keeps the call as tainted as that parameter. What is still reported: opaque calls, a simple class name that two files declare (ambiguous), a class outside the project, a type with an anonymous subclass, and a callee that hands the value to a third class (a callee is read one level deep). A method of the same file called without a receiver is summarised the same way.
// NOT FLAGGED: Sanitizer.clean ignores its argument and returns a literal
String value = sanitizer.clean(request.getParameter("value"));
response.getWriter().println("<p>" + value + "</p>");
// NOT FLAGGED: the constructor argument is a literal, so the getter is too
Holder holder = new Holder("report");
response.getWriter().println("<p>" + holder.get() + "</p>");
// FLAGGED: Echo.same returns its argument
String value = echo.same(request.getParameter("value"));
response.getWriter().println("<p>" + value + "</p>");
// FLAGGED: the getter returns what the constructor was given
Holder holder = new Holder(request.getParameter("value"));
response.getWriter().println("<p>" + holder.get() + "</p>");
What is not flagged
- A value passed through an HTML, attribute or script encoder: ESAPI
encodeForHTML,encodeForHTMLAttribute,encodeForJavaScript; OWASP EncoderEncode.forHtml*andEncode.forJavaScript*; Apache CommonsStringEscapeUtils.escapeHtml3/4andescapeXml*; SpringHtmlUtils.htmlEscape;Jsoup.clean; anyescape*orsanitize*call. - A same-file method that calls one of those encoders, so a wrapper such as
String shown = clean(name)counts as encoding. - A name whose latest unconditional assignment was an encoder call (
name = HtmlUtils.htmlEscape(name)). An encoding inside anifthat is not taken on every path does not count. - A number or boolean parsed from the value (
Integer.parseInt,Long.parseLong,Boolean.parseBoolean) and annotated numeric parameters. - A value that provably holds a constant at the write: a branch a constant condition never takes, a list element or map key that holds a literal, or the result of a same-file method whose every return is constant.
- A response whose content type was set to something other than HTML before the write (
application/json,text/plain,text/csv,MediaType.APPLICATION_JSON,produces = "application/json"), and Spring methods that do not declaretext/html. - Sink names inside comments and string literals, writers that are not the response (
new PrintWriter(file),FileWriter), and test sources.
// NOT FLAGGED: encoded at the point of output
response.getWriter().println("<p>" + Encode.forHtml(name) + "</p>");
// NOT FLAGGED: JSON is not interpreted as HTML
response.setContentType("application/json");
response.getWriter().write("{\"id\": \"" + id + "\"}");
// NOT FLAGGED: a number cannot carry markup
int page = Integer.parseInt(request.getParameter("page"));
response.getWriter().println("<p>Page " + page + "</p>");
Flows into another file, and into another method other than a same-file encoder or a method returning a constant, are not followed. A servlet that never calls setContentType is treated as HTML, which is the default for a writer.
Remediation
Encode for the context at the point of output. Use the encoder that matches where the value lands: HTML body, attribute value, JavaScript string, URL or CSS.
// SAFE: OWASP Java Encoder response.getWriter().println("<p>Hello " + Encode.forHtml(name) + "</p>"); response.getWriter().println("<a title='" + Encode.forHtmlAttribute(name) + "'>x</a>"); response.getWriter().println("<script>var n = '" + Encode.forJavaScript(name) + "';</script>");Use a template engine that escapes by default (Thymeleaf
th:text, JSP with<c:out>orfn:escapeXml, Freemarker auto-escaping) instead of building HTML by hand.<%-- SAFE --%> <p>You searched for: <c:out value="${param.q}"/></p>Return data, not markup. For APIs set
application/jsonand let the client build the page withtextContent. In Spring, return a view name or a JSON object instead of an HTML string.Parse values that must be numbers (
Integer.parseInt) and reject anything else with a 400 response.Add defence in depth. A Content-Security-Policy without
unsafe-inline,HttpOnlyandSameSitecookies reduce the impact of a missed encoder; they do not replace it.