Handling XSS Vulnerabilities in Spring Boot: Request Wrapper, Filters, and Jackson Custom Serialization
This article documents a step‑by‑step solution for preventing XSS attacks in a Spring Boot application, covering input validation, a custom HttpServletRequestWrapper, filter registration, and Jackson serializers/deserializers to escape malicious HTML both on request parameters and JSON payloads.
The purpose of this article is to record how a common XSS vulnerability was solved in a production Spring Boot project. An attacker could inject HTML/JavaScript into a form field, which was stored unchanged and later executed when the data was displayed in a list.
Problem Overview
Example malicious input:
</input><img src=1 onerror=alert1>The code is saved without error, but when the list query renders the value the onerror handler runs, demonstrating how unrestricted input can lead to data theft or other serious issues.
Solution Ideas
Validate input : reject special characters and tags.
Allow input but replace special characters with an empty string.
Allow input and escape special characters.
The first method requires a complete list of forbidden characters and is unfriendly to users. The second method is too aggressive and may corrupt legitimate data. The third method preserves user data while preventing execution, and it is the chosen approach.
Implementation – Backend Filtering
Because the filtering should be applied globally, a servlet filter and a request wrapper are used.
Request Wrapper
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* Re‑wrap the request and override parameter‑retrieving methods to apply XSS escaping.
*/
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();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
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);
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (IOException e) { logger.error("-----get Body String Error:{}----", e.getMessage(), e); }
}
if (reader != null) {
try { reader.close(); } catch (IOException e) { logger.error("-----get Body String Error:{}----", e.getMessage(), e); }
}
}
return builder.toString();
}
}Filter Definition
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Filter that wraps incoming requests 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);
}
}
}Filter Registration
Two ways are shown: using @WebFilter together with @ServletComponentScan , or registering the filter as a Spring bean.
@Bean
public FilterRegistrationBean xssFilterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new XssFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}Jackson Custom Deserializer (for @RequestBody)
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.apache.commons.text.StringEscapeUtils;
import java.io.IOException;
/**
* Deserializer that escapes HTML in JSON string values.
*/
public class XssJacksonDeserializer extends JsonDeserializer
{
@Override
public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return StringEscapeUtils.escapeHtml4(jp.getText());
}
}Jackson Custom Serializer (for response)
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.apache.commons.text.StringEscapeUtils;
import java.io.IOException;
/**
* Serializer that escapes HTML before writing a string to the response.
*/
public class XssJacksonSerializer extends JsonSerializer
{
@Override
public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(StringEscapeUtils.escapeHtml4(value));
}
}Message Converter Configuration
@Override
public void extendMessageConverters(List
> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
ObjectMapper mapper = builder.build();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(String.class, new XssJacksonSerializer());
simpleModule.addDeserializer(String.class, new XssJacksonDeserializer());
mapper.registerModule(simpleModule);
converters.add(new MappingJackson2HttpMessageConverter(mapper));
}Testing
After configuring the wrapper, filter, and Jackson modules, the author submitted the same malicious input. The database stored the escaped version, and the list view displayed the safe string, while the unescaped entry caused an alert.
Additional Note – Handling JSON Strings in @RequestBody
When a raw JSON string is passed as a field, aggressive escaping corrupts the JSON. The deserializer was updated to detect JSON structures (starting with { , [ and ending with } , ] ) and skip escaping for those values.
public class XssJacksonDeserializer extends JsonDeserializer
{
@Override
public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// If the value looks like JSON, return it unchanged.
if (isJson(jp.getText())) {
return jp.getText();
}
return StringEscapeUtils.escapeHtml4(jp.getText());
}
private boolean isJson(String str) {
if (StringUtil.isNotBlank(str)) {
str = str.trim();
if ((str.startsWith("{") && str.endsWith("}")) || (str.startsWith("[") && str.endsWith("]"))) {
return true;
}
}
return false;
}
}Conclusion
Different scenarios require different handling strategies.
Use an interceptor/filter for traditional request parameters and ensure proper registration.
Use custom Jackson serializer/deserializer for @RequestBody payloads, remembering to configure them correctly.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.