Information Security 15 min read

Implementing XSS Prevention in Spring Boot with Request Wrapper, Filter, and Custom Jackson Serialization

This article explains how to protect a Spring Boot application from XSS attacks by validating input, wrapping HttpServletRequest, creating a servlet filter, and customizing Jackson serialization/deserialization to safely handle both form parameters and @RequestBody JSON payloads.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing XSS Prevention in Spring Boot with Request Wrapper, Filter, and Custom Jackson Serialization

What is XSS?

Cross‑site scripting (XSS) occurs when a user submits malicious HTML/JavaScript in a form field, such as </input><img src=1 onerror=alert1> , which is stored unchanged and later executed when the data is rendered.

Solution Ideas

Three common approaches are:

Validate input and reject special characters/tags.

Replace special characters with an empty string before saving.

Escape special characters so the browser treats them as plain text.

The third method is preferred because it preserves user data while preventing execution.

Implementation Overview

The protection is applied on the backend using a request wrapper, a servlet filter, and custom Jackson (de)serializers.

Request Wrapper

/**
 * Re‑wrap the request and override parameter‑retrieving methods to filter each value.
 */
public class XSSHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private static final Logger logger = LoggerFactory.getLogger(XSSHttpServletRequestWrapper.class);
    private HttpServletRequest request;
    private String reqBody;
    public XSSHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        logger.info("---xss XSSHttpServletRequestWrapper created-----");
        this.request = request;
        reqBody = getBodyString();
    }
    @Override
    public String getQueryString() {
        return StringEscapeUtils.escapeHtml4(super.getQueryString());
    }
    @Override
    public String getParameter(String name) {
        logger.info("---xss XSSHttpServletRequestWrapper work  getParameter-----");
        String parameter = request.getParameter(name);
        if (StringUtil.isNotBlank(parameter)) {
            logger.info("----filter before--name:{}--value:{}----", name, parameter);
            parameter = StringEscapeUtils.escapeHtml4(parameter);
            logger.info("----filter after--name:{}--value:{}----", name, parameter);
        }
        return parameter;
    }
    @Override
    public String[] getParameterValues(String name) {
        logger.info("---xss XSSHttpServletRequestWrapper work  getParameterValues-----");
        String[] parameterValues = request.getParameterValues(name);
        if (!CollectionUtil.isEmpty(parameterValues)) {
            for (int i = 0; i < parameterValues.length; i++) {
                parameterValues[i] = StringEscapeUtils.escapeHtml4(parameterValues[i]);
            }
        }
        return parameterValues;
    }
    @Override
    public Map
getParameterMap() {
        logger.info("---xss XSSHttpServletRequestWrapper work  getParameterMap-----");
        Map
map = request.getParameterMap();
        if (map != null && !map.isEmpty()) {
            for (String[] value : map.values()) {
                for (int i = 0; i < value.length; i++) {
                    logger.info("----filter before--value:{}----", value[i]);
                    value[i] = StringEscapeUtils.escapeHtml4(value[i]);
                    logger.info("----filter after--value:{}----", value[i]);
                }
            }
        }
        return map;
    }
    @Override
    public BufferedReader getReader() throws IOException {
        logger.info("---xss XSSHttpServletRequestWrapper work  getReader-----");
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        logger.info("---xss XSSHttpServletRequestWrapper work  getInputStream-----");
        final ByteArrayInputStream bais = new ByteArrayInputStream(reqBody.getBytes(StandardCharsets.UTF_8));
        return new ServletInputStream() {
            @Override public boolean isFinished() { return false; }
            @Override public boolean isReady() { return false; }
            @Override public void setReadListener(ReadListener listener) {}
            @Override public int read() throws IOException { return bais.read(); }
        };
    }
    private String getBodyString() {
        StringBuilder builder = new StringBuilder();
        try (InputStream inputStream = request.getInputStream();
             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
        } catch (IOException e) {
            logger.error("-----get Body String Error:{}----", e.getMessage(), e);
        }
        return builder.toString();
    }
}

Servlet Filter

/**
 * Filter that intercepts requests and wraps them with XSSHttpServletRequestWrapper.
 */
public class XssFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(XssFilter.class);
    @Override public void init(FilterConfig filterConfig) throws ServletException {
        logger.info("----xss filter start-----");
    }
    @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ServletRequest wrapper = null;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest servletRequest = (HttpServletRequest) request;
            wrapper = new XSSHttpServletRequestWrapper(servletRequest);
        }
        if (wrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(wrapper, response);
        }
    }
}

Jackson Custom (De)Serializers

/** Deserializer for String values – escapes HTML unless the value is a JSON string. */
public class XssJacksonDeserializer extends JsonDeserializer
{
    @Override
    public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        String text = jp.getText();
        if (isJson(text)) {
            return text;
        }
        return StringEscapeUtils.escapeHtml4(text);
    }
    private boolean isJson(String str) {
        if (StringUtil.isNotBlank(str)) {
            str = str.trim();
            return (str.startsWith("{") && str.endsWith("}")) || (str.startsWith("[") && str.endsWith("]"));
        }
        return false;
    }
}

/** Serializer for String values – always escapes HTML. */
public class XssJacksonSerializer extends JsonSerializer
{
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeString(StringEscapeUtils.escapeHtml4(value));
    }
}

Registration and Configuration

// Register the filter via Bean
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistrationBean() {
    FilterRegistrationBean<XssFilter> bean = new FilterRegistrationBean<>();
    bean.setFilter(new XssFilter());
    bean.setOrder(1);
    bean.setDispatcherTypes(DispatcherType.REQUEST);
    bean.setEnabled(true);
    bean.addUrlPatterns("/*");
    return bean;
}

// Extend message converters to use custom (de)serializers
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    ObjectMapper mapper = builder.build();
    SimpleModule module = new SimpleModule();
    module.addSerializer(String.class, new XssJacksonSerializer());
    module.addDeserializer(String.class, new XssJacksonDeserializer());
    mapper.registerModule(module);
    converters.add(new MappingJackson2HttpMessageConverter(mapper));
}

Testing

After configuring the wrapper, filter, and serializers, the author submitted the XSS payload </input><img src=1 onerror=alert1> through a form. The stored value in the database was escaped, and the list view displayed the escaped string safely, while a non‑escaped entry still triggered the alert.

Summary

Handle XSS differently for query parameters, form data, and @RequestBody JSON.

Use a servlet filter with a request wrapper to escape parameters early.

Apply custom Jackson (de)serializers to process JSON bodies and responses.

Additional Note

The deserializer was later refined to detect JSON strings (starting with { , [ and ending with } , ] ) and skip escaping, preventing double‑escaping of legitimate JSON payloads.

JavaSpring BootsecurityXSSJacksonFilterRequestWrapper
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.