表现
有如下两个接口:
@PostMapping
public R<?> save(@RequestBody @Valid ArticleDTO dto) {
return R.ok(service.saveArticle(dto));
}
@PutMapping
public R<?> updateById(@RequestBody @Valid ArticleDTO dto) {
return R.ok(service.updateArticle(dto));
}
类似的请求参数(同一个页面的表单), 保存时 dto 中大部分属性都是 NULL
更新时则没有任何问题,
确认请求参数已发出, Spring MVC org.springframework.web.servlet.DispatcherServlet
中断点还能看到, 到达Controller
中参数丢失
前提条件:
请求参数包含一个特殊属性: coverUrl
与之对应的控制器入参对象中 coverUrl
类型为 String
(AVue Upload 类型表单组件默认值)
{
...
"coverUrl": [],
"images": [],
...
}
项目中 ....common.xss.core.JacksonXssClean
重写了 com.fasterxml.jackson.databind.JsonDeserializer#deserialize
方法, 处理所有的反序列化
deserialize
方法实现如下:
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// XSS filter
String text = p.getValueAsString();
if (text == null) {
return null;
} else if (XssHolder.isEnabled()) {
String value = XssUtil.clean(text);
log.trace("Json property value:{} cleaned up by mica-xss, current value is:{}.", text, value);
return value;
} else {
return text;
}
}
万恶之源
JsonParser.getValueAsString()
方法注释:
方法将尝试将当前标记的值转换为{@link java.lang.String}。JSON字符串自然映射;标量值被转换为它们的文本表示形式。如果表示不能转换为字符串值(包括结构化类型,如对象、数组和空令牌),将返回默认值null;没有抛出异常。
当处理到请求参数: coverUrl
的属性 [
时, getValueAsString()
直接返回 null
导致方法退出. 后续没有抛出异常, 而是中断所有解析! 也就导致了 content
属性没能正确赋值
而更新方法携带的 coverUrl 属性是查询是返回的, 默认为 NULL, 会跳过 deserialize()
方法的解析. 如果查询结果 coverUrl
非 NULL, 再次提交同样会丢失 coverUrl
之后解析的所有属性 (解析顺序未知, 和入参类型属性的定义顺序, 请求参数的传递顺序 无关)
2021-09-11 02:15:23.502 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
VALUE_STRING type ArticleDTO(images=null) 2 2
2021-09-11 02:15:23.624 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: 2 过滤后: 2
2021-09-11 02:15:23.625 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
VALUE_STRING title ArticleDTO(images=null) 文章标题 文章标题
2021-09-11 02:15:23.625 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: 文章标题 过滤后: 文章标题
2021-09-11 02:15:23.625 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
START_ARRAY coverUrl NULL [ [
2021-09-11 02:15:23.625 ERROR 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 下一个令牌: END_ARRAY
2021-09-11 02:15:23.625 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: [ 过滤后: [
2021-09-11 02:15:23.626 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
VALUE_STRING profile ArticleDTO(images=[])
2021-09-11 02:15:23.626 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: 过滤后:
2021-09-11 02:15:23.626 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
VALUE_STRING content ArticleDTO(images=[]) <p>详情内容</p> <p>详情内容</p>
2021-09-11 02:15:23.629 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: <p>详情内容</p> 过滤后: <p>详情内容</p>
2021-09-11 02:15:23.631 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
VALUE_STRING visibleStatus ArticleDTO(images=[]) 0 0
2021-09-11 02:15:23.632 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: 0 过滤后: 0
2021-09-11 02:15:23.633 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean :
JSON Token 当前属性名 当前值 当前文本 ValueAsString(NULL)
VALUE_STRING validStatus ArticleDTO(images=[]) 0 0
2021-09-11 02:15:23.633 WARN 10560 --- [ XNIO-1 task-1] n.v.gms.common.xss.core.JacksonXssClean : 触发XSS过滤, 原: 0 过滤后: 0
2021-09-11 02:15:23.775 INFO 10560 --- [ XNIO-1 task-1] n.v.g.cms.controller.ArticleController : 新增文章: {"visitorTotal":0,"images":[],"profile":"","sort":9999,"title":"文章标题","type":"2","content":"<p>详情内容</p>","coverUrl":"[","visibleStatus":"0","validStatus":"0","channelId":0}
解决方案
前端页面
删除 coverUrl
属性即可
后端
一个有限的解决方案, 只能处理空数组, 修改 deserialize
方法实现:
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// XSS filter
// String text = p.getValueAsString();
// Method can be called for any token type.
String text = p.getText();
// 当前 token 为数组开始字符
if (p.isExpectedStartArrayToken()) {
// 快进到下一个 token
JsonToken jsonToken = p.nextToken();
}
if (text == null) {
return null;
} else if (XssHolder.isEnabled()) {
String value = XssUtil.clean(text);
log.trace("Json property value:{} cleaned up by mica-xss, current value is:{}.", text, value);
return value;
} else {
return text;
}
}
对于非空数组, if 中的 jsonToken
会得到 com.fasterxml.jackson.core.JsonToken#VALUE_STRING
类型枚举, 然后在退出方法后报错