Spring过滤json中的XSS

本文介绍如何在Spring MVC中使用自定义的XSSMappingJackson2HttpMessageConverter来处理JSON的XSS攻击,通过重写read()和writeInternal()方法,确保了请求和响应的数据安全。

spring处理json是通过MappingJackson2HttpMessageConverter实现的。

而MappingJackson2HttpMessageConverter中的read()和writeInternal()分别对应json的请求和响应。

也就是说我们的过滤工作就这两个方法中展开。

writeInternal首先创建一个类并继承MappingJackson2HttpMessageConverter,我创建的是XSSMappingJackson2HttpMessageConverter

在spring-mvc.xml中找到

<bean id="mappingJacksonHttpMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">

//中间省略

</bean>

将MappingJackson2HttpMessageConverter替换成XSSMappingJackson2HttpMessageConverter

打开XSSMappingJackson2HttpMessageConverter,重写read()

@Override
public Object read(Type type, Class<?> contextClass,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
Object obj = readJavaType(javaType, inputMessage);
String json = super.getObjectMapper().writeValueAsString(obj);
json = cleanXSS(json.toString());
Object obj1 = super.getObjectMapper().readValue(json, javaType);
return obj1;
}


// 这个就是父类的readJavaType方法,由于父类该方法是private的,所以我们copy一个用
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
try {
return super.getObjectMapper().readValue(inputMessage.getBody(),
javaType);
} catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: "
+ ex.getMessage(), ex);
}
}


private String cleanXSS(String value) {
return Jsoup.clean(value.toString(), Whitelist.relaxed());
}

以上RequestBody的XSS过滤就完成,接下来是ResponseBody的过滤

// 重写writeInternal方法,在返回内容前首先进行加密
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// 使用Jackson的ObjectMapper将Java对象转换成Json String
String json = super.getObjectMapper().writeValueAsString(object);
String result = cleanXSS(json.toString());
// 输出
outputMessage.getBody().write(result.getBytes());
}

完整代码

package com.meadin.funding.filter;


import java.io.IOException;
import java.lang.reflect.Type;


import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;


import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;


public class XSSMappingJackson2HttpMessageConverter extends
MappingJackson2HttpMessageConverter {


@Override
public Object read(Type type, Class<?> contextClass,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
Object obj = readJavaType(javaType, inputMessage);
String json = super.getObjectMapper().writeValueAsString(obj);
String result = cleanXSS(json.toString());
Object resultObj = super.getObjectMapper().readValue(result, javaType);
return resultObj;
}


// 这个就是父类的readJavaType方法,由于父类该方法是private的,所以我们copy一个用
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
try {
return super.getObjectMapper().readValue(inputMessage.getBody(),
javaType);
} catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: "
+ ex.getMessage(), ex);
}
}


// 重写writeInternal方法,在返回内容前首先进行加密
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// 使用Jackson的ObjectMapper将Java对象转换成Json String
String json = super.getObjectMapper().writeValueAsString(object);
String result = cleanXSS(json.toString());
// 输出
outputMessage.getBody().write(result.getBytes());
}


private String cleanXSS(String value) {
return Jsoup.clean(value.toString(), Whitelist.relaxed());
}
}

<!--避免IE执行AJAX时,返回JSON出现下载文件 -->
<bean id="mappingJacksonHttpMessageConverter"
class="com.meadin.funding.filter.XSSMappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<!-- 要返回json格式正常必须设置为application/json;charset=UTF-8 -->
<value>application/json;charset=UTF-8</value>
</list>
</property>
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<!-- 属性值为null时隐藏 -->
<property name="serializationInclusion">
<value type="com.fasterxml.jackson.annotation.JsonInclude.Include">NON_NULL</value>
</property>
<!-- 处理responseBody 里面日期类型 -->
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg type="java.lang.String" value="yyyy-MM-dd HH:mm:ss" />
</bean>
</property>
</bean>
</property>
</bean>
<!-- 配合在实体类中的注解,将string类型自动转换成date类型存储 -->
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="mappingJacksonHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>

有关资料:

http://www.scienjus.com/custom-http-message-converter/

http://www.scienjus.com/spring-mvc-use-skill/#disqus_thread

http://ju.outofmemory.cn/entry/63726

http://blog.youkuaiyun.com/cdnight/article/details/24415275

<think>我们正在讨论的是在Java Web应用中如何对JSON数据进行XSS过滤。根据引用内容,有两种主要方法: 1. 通过修改SpringMVCjson序列化器(如Jackson)来在序列化过程中过滤XSS(引用[1]提到)。 2. 通过过滤器(Filter)在请求进入时对参数进行过滤(引用[3]提到)。 但是,对于JSON数据,由于通常是通过请求体(@RequestBody)传递的,在过滤器中直接读取请求体会导致Controller无法再次读取(因为输入流只能读一次)。因此,我们需要结合之前解决请求体重复读取的方法(使用请求包装器)和XSS过滤。 具体步骤: 1. 创建一个可缓存请求体的HttpServletRequestWrapper(如前所述)。 2. 在过滤器中,使用这个包装器,然后从缓存的请求体中获取JSON字符串。 3. 对JSON字符串进行XSS过滤(可以使用像org.apache.commons.text.StringEscapeUtils这样的库,或者自定义过滤规则)。 4. 将过滤后的字符串重新设置到请求包装器中(或者创建一个新的包装器,包含过滤后的字节数组)。 5. 将包装后的请求传递给后续处理。 然而,需要注意的是,直接修改JSON字符串可能会破坏JSON结构(例如,转义了引号等)。因此,更推荐的方式是解析JSON树,然后对字符串类型的节点进行过滤。 另一种方法(如引用[1]所述)是在Jackson的序列化/反序列化过程中进行过滤,这通常更安全,因为它是在解析为对象树后进行的,不会破坏结构。 下面我将分别介绍这两种方法。 方法一:使用过滤器(结合请求包装器和JSON解析) 步骤: 1. 创建可缓存请求体的请求包装器(CachedBodyHttpServletRequest,如前所述)。 2. 在过滤器中,读取缓存的请求体(JSON字符串),然后解析为JSON树(如使用Jackson的ObjectMapper)。 3. 遍历JSON树,对所有字符串值进行XSS过滤(例如,转义HTML特殊字符)。 4. 将过滤后的JSON树写回为字符串,并更新请求包装器中的缓存体。 5. 将包装后的请求传递下去。 方法二:使用Jackson的JsonDeserializer(在反序列化时过滤) 1. 自定义一个JsonDeserializer,用于在反序列化字符串时进行XSS过滤。 2. 将这个反序列化器注册到ObjectMapper中,可以全局注册或者通过注解在特定字段上使用。 考虑到方法一在过滤器中处理所有请求,可能会影响性能(因为要解析和重构整个JSON),而且如果请求体不是JSON,则不需要处理。方法二则更精确,只在反序列化时处理字符串字段。 下面分别给出两种方法的实现示例。 方法一:过滤器方式(适用于所有请求,但注意性能) 首先,我们扩展之前的CachedBodyHttpServletRequest,使其可以重新设置缓存体(用于过滤后更新)。 修改CachedBodyHttpServletRequest: 增加一个setCachedBody方法,用于更新缓存体。 然后,在过滤器中: 1. 包装请求。 2. 检查请求的Content-Type是否为application/json(避免处理非JSON请求)。 3. 读取缓存体,解析为JSON(使用Jackson的JsonNode)。 4. 遍历JsonNode,对所有文本节点进行过滤。 5. 将过滤后的JsonNode写回字符串,并设置回请求包装器。 6. 传递包装后的请求。 方法二:使用Jackson反序列化器(推荐,因为更轻量且精确) 步骤: 1. 自定义一个XSS过滤的反序列化器。 2. 注册这个反序列化器到ObjectMapper(可以全局注册,也可以针对特定字段使用注解)。 下面分别给出两种方法的代码示例。 方法一:过滤器方式 注意:这种方法需要处理整个请求体,可能会对性能有影响,特别是大JSON。 步骤1:修改CachedBodyHttpServletRequest,增加setCachedBody方法(可选,也可以重新创建字节数组) 实际上,我们可以通过重新设置cachedBody并重新生成输入流和reader。我们在之前的类中增加一个setCachedBody方法: ```java public void setCachedBody(byte[] body) { this.cachedBody = body; } ``` 步骤2:编写XSS过滤工具类(简单示例,使用StringEscapeUtils) 添加依赖(如果使用Apache Commons Text): ```xml <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.9</version> </dependency> ``` 步骤3:编写过滤器,对JSON请求体进行过滤 ```java import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.text.StringEscapeUtils; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class XssJsonFilter implements Filter { private ObjectMapper objectMapper = new ObjectMapper(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 检查是否为JSON请求 if (isJsonRequest(httpRequest)) { // 使用我们之前定义的包装器 CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest(httpRequest); // 读取缓存体 byte[] bodyBytes = wrappedRequest.getCachedBody(); String bodyStr = new String(bodyBytes, httpRequest.getCharacterEncoding()); // 解析JSON JsonNode rootNode = objectMapper.readTree(bodyStr); // 遍历JSON树并过滤字符串值 filterJsonNode(rootNode); // 将过滤后的JSON转回字符串 String filteredBody = objectMapper.writeValueAsString(rootNode); // 重新设置缓存体 wrappedRequest.setCachedBody(filteredBody.getBytes(httpRequest.getCharacterEncoding())); // 继续传递包装后的请求 chain.doFilter(wrappedRequest, response); } else { chain.doFilter(request, response); } } private boolean isJsonRequest(HttpServletRequest request) { String contentType = request.getContentType(); return contentType != null && contentType.contains("application/json"); } // 递归遍历JsonNode,对字符串值进行过滤 private void filterJsonNode(JsonNode node) { if (node.isObject()) { node.fields().forEachRemaining(entry -> { JsonNode value = entry.getValue(); if (value.isTextual()) { // 对字符串进行XSS过滤 String filtered = StringEscapeUtils.escapeHtml4(value.asText()); ((ObjectNode) node).put(entry.getKey(), filtered); } else { filterJsonNode(value); // 递归处理 } }); } else if (node.isArray()) { for (JsonNode item : node) { filterJsonNode(item); } } // 其他类型无需处理 } } ``` 注意:上面的递归过滤中,我们只处理了对象和数组。对于基本类型,只处理字符串。 方法二:使用Jackson反序列化器(推荐) 步骤1:创建自定义反序列化器 ```java 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; public class XssStringJsonDeserializer extends JsonDeserializer<String> { @Override public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String value = p.getValueAsString(); if (value == null) { return null; } // 进行XSS过滤 return StringEscapeUtils.escapeHtml4(value); } } ``` 步骤2:注册这个反序列化器 有两种方式: 方式一:全局注册(所有String类型字段都应用) ```java import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); SimpleModule module = new SimpleModule(); module.addDeserializer(String.class, new XssStringJsonDeserializer()); objectMapper.registerModule(module); return objectMapper; } } ``` 方式二:在特定字段上使用注解 ```java public class User { @JsonDeserialize(using = XssStringJsonDeserializer.class) private String name; // 其他字段... } ``` 方法二的好处: - 只在反序列化时处理,不影响其他请求。 - 只针对字符串字段,不会误处理数字等。 - 更符合单一职责原则。 但是,方法二需要确保所有需要过滤的字段都应用了该反序列化器(如果是全局注册则所有字符串字段都会被过滤,可能有些字段不需要过滤,比如密码字段通常不需要转义HTML,但需要防止存储型XSS,这取决于上下文)。 因此,建议使用方法二,并考虑在不需要过滤的字段上使用其他注解(如@JsonRawValue)或通过自定义注解来排除。 总结: - 如果希望集中处理所有请求的JSON数据,可以使用过滤器方法(方法一),但要注意性能。 - 如果使用Spring Boot,推荐使用方法二(自定义Jackson反序列化器),因为它更精确,且不会破坏JSON结构,同时只在需要的地方过滤。 根据引用[1]中提到的,他们使用了修改json序列化的方式(即类似方法二),所以方法二也是被验证可行的。 注意:XSS过滤的规则可以根据需要调整,比如使用更严格的策略(如白名单)或使用专门的库(如OWASP Java Encoder)。 使用OWASP Java Encoder的例子: ```java import org.owasp.encoder.Encode; // 在反序列化器中 return Encode.forHtmlContent(value); // 根据上下文选择编码方法 ``` 相关问题: 1. 如何选择XSS过滤的编码方式(HTML内容、HTML属性、JavaScript等)? 2. 在Spring Boot中如何排除某些字段不进行XSS过滤? 3. 如何防止GET请求中的XSS攻击(URL参数)?</think>### Java Web应用中JSON数据的XSS过滤方法 在Java Web应用中防止JSON数据的XSS攻击,主要有两种核心方案:**过滤器层过滤**和**JSON序列化层过滤**。以下是具体实现方法: --- #### 方案1:过滤器层过滤(推荐) 在请求到达Controller前进行XSS过滤,兼容性强且不依赖特定框架。 ##### 实现步骤: 1. **创建XSS过滤工具类**: ```java import org.apache.commons.text.StringEscapeUtils; public class XssUtils { // 使用ESAPI或Apache Commons Text进行XSS过滤 public static String cleanXss(String value) { if (value == null) return null; return StringEscapeUtils.escapeHtml4(value); // 转义HTML特殊字符 // 或使用:ESAPI.encoder().encodeForHTML(value) // 需ESAPI依赖 } } ``` 2. **创建可缓存请求的包装器**(解决请求体多次读取问题): ```java import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.nio.charset.StandardCharsets; public class XssRequestWrapper extends HttpServletRequestWrapper { private byte[] cachedBody; public XssRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 读取并缓存请求体 ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream is = request.getInputStream(); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) > -1) { baos.write(buffer, 0, len); } cachedBody = baos.toByteArray(); } @Override public ServletInputStream getInputStream() { return new CachedServletInputStream(cachedBody); } // 获取过滤后的JSON字符串 public String getFilteredJson() { String json = new String(cachedBody, StandardCharsets.UTF_8); return XssUtils.cleanXss(json); // 应用XSS过滤 } } ``` 3. **创建XSS过滤器**: ```java import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class XssFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) req; // 只处理JSON请求 if ("application/json".equalsIgnoreCase(httpReq.getContentType())) { XssRequestWrapper wrappedRequest = new XssRequestWrapper(httpReq); // 获取过滤后的JSON String filteredJson = wrappedRequest.getFilteredJson(); // 将过滤后的JSON重新放入请求属性(Controller层可获取) wrappedRequest.setAttribute("filteredBody", filteredJson); chain.doFilter(wrappedRequest, res); } else { chain.doFilter(req, res); // 非JSON请求直接放行 } } } ``` 4. **在Controller中获取过滤后的数据**: ```java @PostMapping("/submit") public ResponseEntity<?> handleJson(@RequestBody String rawJson, HttpServletRequest request) { // 从请求属性获取已过滤JSON String safeJson = (String) request.getAttribute("filteredBody"); // 使用safeJson继续处理... return ResponseEntity.ok("Processed safely"); } ``` 5. **配置过滤器**(web.xml): ```xml <filter> <filter-name>XssFilter</filter-name> <filter-class>com.example.XssFilter</filter-class> </filter> <filter-mapping> <filter-name>XssFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ``` --- #### 方案2:JSON序列化层过滤Spring Boot专用) 通过自定义Jackson反序列化器在JSON解析时过滤XSS,更精准但依赖Spring框架。 ##### 实现步骤: 1. **创建XSS反序列化器**: ```java import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import org.apache.commons.text.StringEscapeUtils; import java.io.IOException; public class XssStringDeserializer extends JsonDeserializer<String> { @Override public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException { String value = p.getValueAsString(); return value != null ? StringEscapeUtils.escapeHtml4(value) : null; } } ``` 2. **注册全局反序列化器**: ```java import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JacksonConfig { @Bean public Module xssModule() { SimpleModule module = new SimpleModule(); // 所有String类型字段都应用XSS过滤 module.addDeserializer(String.class, new XssStringDeserializer()); return module; } } ``` 3. **选择性过滤**(使用注解): ```java public class UserDto { @JsonDeserialize(using = XssStringDeserializer.class) // 仅过滤特定字段 private String bio; private Integer age; // 非字符串字段不受影响 } ``` --- ### 两种方案对比 | **方案** | **优势** | **适用场景** | |---------------------|----------------------------------|------------------------------| | 过滤器层过滤 | 框架无关、支持非JSON请求 | 多框架项目、需要统一安全管控 | | JSON序列化层过滤 | 精准到字段、无需处理请求流问题 | Spring Boot项目、精确控制 | --- ### 关键注意事项 1. **性能优化**: - 对于大JSON,使用流式解析(如Jackson的`JsonParser`) - 添加缓存大小限制:`if(cachedBody.length > 10_000_000) throw new PayloadTooLargeException()` 2. **上下文感知过滤**: ```java // 根据内容位置选择不同编码方式 String sanitized = switch(context) { case HTML -> Encode.forHtml(input); case JS -> Encode.forJavaScript(input); default -> input; }; ``` 3. **依赖库推荐**: ```xml <!-- Apache Commons Text --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.10.0</version> </dependency> <!-- OWASP Java Encoder(更专业) --> <dependency> <groupId>org.owasp.encoder</groupId> <artifactId>encoder</artifactId> <version>1.2.3</version> </dependency> ``` 4. **测试用例**: ```java @Test void testXssFilter() { String malicious = "<script>alert(1)</script>"; String safe = XssUtils.cleanXss(malicious); assertEquals("<script>alert(1)</script>", safe); } ``` > 实际项目中建议结合两种方案: > - 用过滤器处理所有请求的通用XSS防护 > - 用JSON反序列化器对敏感字段额外加固[^1][^3] --- ### 总结 在Java Web应用中防护JSON数据的XSS攻击,**过滤器方案**提供基础防护并解决请求流复用问题,**JSON序列化方案**实现精准字段级过滤。关键点是: 1. 通过`HttpServletRequestWrapper`缓存请求体 2. 使用`StringEscapeUtils`或OWASP Encoder转义危险字符 3. 在过滤器或Jackson反序列化器中植入清洗逻辑 4. 对HTML/JS/URL上下文采用差异化编码策略[^2] 对于新项目推荐使用JSON序列化层方案,遗留系统建议采用过滤器方案逐步迁移[^1][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值