flawopen.com/xss/Java
Java XSS: Secure Spring MVC and Thymeleaf Output
th:utext and raw JSP output can execute injected JavaScript. Encode for each output context and sanitize rich text with an allow-list (CWE-79).
A stadium scoreboard has a plain name field and a separate rich-display mode. Thymeleaf’s th:text treats a submitted name as literal text; th:utext tells the renderer to treat the value as HTML instructions for the board. The browser receives the stadium site’s text/html and renders that markup with the site’s authority. Keep user text in escaped output, and sanitize it before using an unescaped template slot when rich formatting is required.
Core Concepts & Subsystem Terms
Thymeleaf th:text vs th:utextth:textHTML-escapes its value;th:utext(unescaped text) inserts it as markup. In Thymeleaf's inlined form,[[${x}]]escapes and[(${x})]does not. Ath:utextbound to user input is XSS unless the value was sanitized first.JSP ${} vs <c:out>- In JSP template text,
${param.q}is written without escaping.<c:out value="${param.q}"/>or${fn:escapeXml(param.q)}escapes it. Scriptlets such as<%= request.getParameter("q") %>never escape. Encode.forHtml / forJavaScript- The OWASP Java Encoder has one method per output context:
Encode.forHtmlfor element content,forHtmlAttributefor quoted attributes,forJavaScriptfor JavaScript string literals andforUriComponentfor URL parameters. HTML-encoding a value placed inside a script is not enough. OWASP Java HTML Sanitizer- For user-supplied HTML, a policy such as
Sanitizers.FORMATTING.and(Sanitizers.LINKS)keeps an allowlist of tags and attributes and drops scripts, event handlers andjavascript:URLs. Encoding would show the tags as text; sanitizing keeps the safe ones working. @ResponseBody returning text/html- A
@RestControllermethod that returns aStringwithproduces = text/htmlis written to the response as-is: no template engine runs, so nothing is escaped. Prefer a view template, or encode every value you concatenate.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Input arrives in a request parameter
Spring binds q, user or bio from the query string or form body into a String argument. A victim can be sent a crafted link, or a submitted bio is shown to other users.
Concatenation builds the response
The controller glues the value into HTML or into a <script> block. With the benign test value <img src=x onerror=alert(1)>, the returned string contains a real tag; with ';alert(1);// the script block gains a new statement.
The browser parses it
Because the method declares text/html, the browser renders the body as a page. The image fails and fires onerror, or the script block runs the injected statement.
The script runs as the site
The code executes with the application's origin: it can read the DOM, call the application's endpoints with the victim's session cookie and change what the user sees.
Source Code: Flaw vs. Secure Implementation
// ProfileController.java: each endpoint concatenates request data into HTML
@RestController
public class ProfileController {
// HTML context: the query value is inserted as markup
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + q + "</h1>";
}
// JavaScript context: a quote in user ends the string literal
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + user + "';</script>";
}
// rich text is echoed with every tag and handler intact
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + bio + "</div>";
}
}
// ProfileController.java: each value is encoded for its context or sanitized
@RestController
public class ProfileController {
private static final PolicyFactory BIO_POLICY = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
// forHtml encodes < > & " ' for element content
@GetMapping(value = "/search", produces = MediaType.TEXT_HTML_VALUE)
public String search(@RequestParam String q) {
return "<h1>Results for " + Encode.forHtml(q) + "</h1>";
}
// forJavaScript escapes quotes, backslashes and </script> sequences
@GetMapping(value = "/widget", produces = MediaType.TEXT_HTML_VALUE)
public String widget(@RequestParam String user) {
return "<script>const user = '" + Encode.forJavaScript(user) + "';</script>";
}
// the sanitizer keeps formatting and links and drops everything else
@PostMapping(value = "/preview", produces = MediaType.TEXT_HTML_VALUE)
public String preview(@RequestParam String bio) {
return "<div class=\"bio\">" + BIO_POLICY.sanitize(bio) + "</div>";
}
}
Engineering & System Hardening Checklist
- ✓Enable Find Security Bugs in SpotBugs and fail the build on
XSS_SERVLET,XSS_JSP_PRINTandXSS_REQUEST_PARAMETER_TO_SERVLET_WRITER. - ✓Grep templates in CI for
th:utext,[(,<%=and bare${...}in JSP; each hit must render sanitized or constant content. - ✓Encode for the exact context with the OWASP Java Encoder (
forHtml,forHtmlAttribute,forJavaScript,forUriComponent) whenever a controller builds markup itself. - ✓Pass user-supplied HTML through an OWASP Java HTML Sanitizer policy before it reaches
th:utextor a response body. - ✓Configure Spring Security's
headers().contentSecurityPolicy(...)with a nonce-basedscript-srcand keep the session cookieHttpOnlyandSameSite=Lax.