前言
项目安全需要, 需要全局对参数进行xss过滤处理.
Xss简介
关于Xss很多人可能都有了解, 出于“礼貌”,
咸鱼君还是简单举个例子
用户注册时可以填写姓名
此时我填写了“<script>alert(1);</script>” 并提交,
后端呢没有做任何检测就保存了.
那么就可能有问题, 下次再访问这个页面你会发现不停的弹窗“1”!
Xss攻击呢也分很多种, 感兴趣的自行查阅资料, 这里不多说了.
那么如何面对Xss攻击呢?
其实也很好解决, 一句话“不要相信用户的任何输入”!
编程上就是对用户的提交内容进行过滤以及非法字符的“转义”!
Springboot Xss拦截器实现
对整个系统的提交进行过滤和转义, 如过针对每个点分别调用某个方法去做这件事, 会显得很麻烦, 而且, 重复的代码看着也不美观, 所以, 这里就采用Springboot+Filter的方式实现一个Xss的全局过滤器.
Springboot实现一个Xss过滤器, 常用的有两种方式:
- 重写HttpServletRequestWrapper
重写getHeader()、getParameter()、getParameterValues()、getInputStream()实现对传统“键值对”传参方式的过滤
重写getInputStream()实现对Json方式传参的过滤,也就是@RequestBody参数.
- 自定义序列化器, 对MappingJackson2HttpMessageConverter 的objectMapper做设置.
重写JsonSerializer.serialize()实现对出参的过滤 (PS: 数据原样保存, 取出来的时候转义)
重写JsonDeserializer.deserialize()实现对入参的过滤 (PS: 数据转义后保存)
针对以上两种方式有几个注意点:
问题一: json参数(@RequestBody)的处理
针对Json方式传参 ,
springmvc默认使用jackjson做序列化.
重写getInputStream()来实现xss过滤时,
如果你对参数替换了双引号, jackjson序列/反序列化参数时会报错,
因为它不认识这个格式!
所以我们对参数处理时要剔除双引号!
或者, 你选择自定义序列化器来实现json参数的处理.
问题二: 自定义序列化器
虽然我们入参和出参两种方式都写了案例,
实际上只需要选择其一来做即可!没必要两种都做过滤.
这里推荐对入参进行处理, 理由如下:
重写getHeader()、getParameter()、getParameterValues()是直接对入参进行转义,
也就是你后续程序获取的参数都是转义后的,
所以, 为了全局统一, 我们对@RequestBody类型的参数也进行入参处理.对入参转义意味着保存进DB的就是“安全”的数据
本次案例实现原理如下:
- 针对“键值对”参数采用重写HttpServletRequestWrapper过滤
- 针对json参数采用自定义序列化器来过滤
PS: 针对json参数也实现了getInputStream()方式的过滤(代码为注释状态)
实现
首先, 我们先引入一个工具包, 就是大名鼎鼎的“糊涂”工具包, 该包集成了大量的好用的工具!强烈推荐使用!
<!--HuTool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
我们用到HuTool内的EscapeUtil这个工具来转义特殊字符.
使用HttpServletRequestWrapper重写Request请求参数
package com.mrcoder.sbxssfilter.config.xss;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* XSS过滤处理
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* 描述 : 构造函数
*
* @param request 请求对象
*/
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return EscapeUtil.escape(value);
}
//重写getParameter
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return EscapeUtil.escape(value);
}
//重写getParameterValues
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapseValues = new String[length];
for (int i = 0; i < length; i++) {
escapseValues[i] = EscapeUtil.escape(values[i]);
}
return escapseValues;
}
return super.getParameterValues(name);
}
// //重写getInputStream,对json格式参数进行过滤(也就是@RequestBody类型的参数)
// @Override
// public ServletInputStream getInputStream() throws IOException {
// // 非json类型,直接返回
// if (!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
// return super.getInputStream();
// }
//
// // 为空,直接返回
// String json = IoUtil.read(super.getInputStream(), "utf-8");
// if (StrUtil.isEmpty(json)) {
// return super.getInputStream();
// }
// // 这里要注意,json格式的参数不能直接使用hutool的EscapeUtil.escape, 因为它会把"也给转义,
// // 使得@RequestBody没办法解析成为一个正常的对象,所以我们自己实现一个过滤方法
// // 或者采用定制自己的objectMapper处理json出入参的转义(推荐使用)
// json = cleanXSS(json).trim();
// final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
// return new ServletInputStream() {
// @Override
// public boolean isFinished() {
// return true;
// }
//
// @Override
// public boolean isReady() {
// return true;
// }
//
// @Override
// public void setReadListener(ReadListener readListener) {
// }
//
// @Override
// public int read() {
// return bis.read();
// }
// };
// }
//
// public static String cleanXSS(String value) {
// value = value.replaceAll("&", "%26");
// value = value.replaceAll("<", "%3c");
// value = value.replaceAll(">", "%3e");
// value = value.replaceAll("'", "%27");
// //value = value.replaceAll(":", "%3a");
// //value = value.replaceAll("\"", "%22");
// //value = value.replaceAll("/", "%2f");
// return value;
// }
}
实现一个过滤器
package com.mrcoder.sbxssfilter.config.xss;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 防止XSS攻击的过滤器
*/
@Component
public class XssFilter implements Filter {
/**
* 排除链接
*/
private List<String> excludes = new ArrayList<>();
/**
* xss过滤开关
*/
private boolean enabled = false;
@Override
public void init(FilterConfig filterConfig) {
String tempExcludes = filterConfig.getInitParameter("excludes");
String tempEnabled = filterConfig.getInitParameter("enabled");
if (StrUtil.isNotEmpty(tempExcludes)) {
String[] url = tempExcludes.split(",");
Collections.addAll(excludes, url);
}
if (StrUtil.isNotEmpty(tempEnabled)) {
enabled = Boolean.valueOf(tempEnabled);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if (handleExcludeUrl(req)) {
chain.doFilter(request, response);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}
/**
* 判断当前路径是否需要过滤
*/
private boolean handleExcludeUrl(HttpServletRequest request) {
if (!enabled) {
return true;
}
if (excludes == null || excludes.isEmpty()) {
return false;
}
String url = request.getServletPath();
for (String pattern : excludes) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}
}
配置并注册过滤器
package com.mrcoder.sbxssfilter.config.xss;
import javax.servlet.DispatcherType;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.util.HashMap;
import java.util.Map;
/**
* @Author xssfilter配置
*/
@Configuration
public class XssFilterConfig {
@Value("${xss.enabled}")
private String enabled;
@Value("${xss.excludes}")
private String excludes;
@Value("${xss.urlPatterns}")
private String urlPatterns;
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
//添加过滤路径
registration.addUrlPatterns(StrUtil.split(urlPatterns, ","));
registration.setName("xssFilter");
registration.setOrder(Integer.MAX_VALUE);
//设置初始化参数
Map<String, String> initParameters = new HashMap<String, String>();
initParameters.put("excludes", excludes);
initParameters.put("enabled", enabled);
registration.setInitParameters(initParameters);
return registration;
}
/**
* 过滤json类型的
*
* @param builder
* @return
*/
@Bean
@Primary
public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
//解析器
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
//注册xss解析器
SimpleModule xssModule = new SimpleModule("XssStringJsonDeserializer");
//入参和出参过滤选一个就好了,没必要两个都加
//这里为了和XssHttpServletRequestWrapper统一,建议对入参进行处理
//注册入参转义
xssModule.addDeserializer(String.class, new XssStringJsonDeserializer());
//注册出参转义
//xssModule.addSerializer(new XssStringJsonSerializer());
objectMapper.registerModule(xssModule);
//返回
return objectMapper;
}
}
最后,我们在配置文件加上
#是否打开
xss.enabled=true
#不过滤路径, 以逗号分割
xss.excludes=/open/*,/open2/*
//过滤路径, 逗号分割
xss.urlPatterns=/*
另外, 我们需要实现一个ObjectMapper来处理json格式的参数
处理json入参的转义
package com.mrcoder.sbxssfilter.config.xss;
import cn.hutool.core.util.EscapeUtil;
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 java.io.IOException;
/**
* 处理json入参的转义
*/
public class XssStringJsonDeserializer extends JsonDeserializer<String> {
@Override
public Class<String> handledType() {
return String.class;
}
//对入参转义
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String value = jsonParser.getText();
if (value != null) {
return EscapeUtil.escape(value);
}
return value;
}
}
处理json出参的转义
package com.mrcoder.sbxssfilter.config.xss;
import cn.hutool.core.util.EscapeUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
* 处理json出参的转义
*/
public class XssStringJsonSerializer extends JsonSerializer<String> {
@Override
public Class<String> handledType() {
return String.class;
}
//对出参转义
@Override
public void serialize(String value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
if (value != null) {
String encodedValue = EscapeUtil.escape(value);
jsonGenerator.writeString(encodedValue);
}
}
}
写个测试
package com.mrcoder.sbxssfilter.model;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class People {
private String name;
private String info;
}
package com.mrcoder.sbxssfilter.controller;
import com.mrcoder.sbxssfilter.model.People;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class XssController {
//键值对
@PostMapping("xssFilter")
public String xssFilter(String name, String info) {
log.error(name + "---" + info);
return name + "---" + info;
}
//实体
@PostMapping("modelXssFilter")
public People modelXssFilter(@RequestBody People people) {
log.error(people.getName() + "---" + people.getInfo());
return people;
}
//不转义
@PostMapping("open/xssFilter")
public String openXssFilter(String name) {
return name;
}
//不转义2
@PostMapping("open2/xssFilter")
public String open2XssFilter(String name) {
return name;
}
}
json方式
键值对方式
项目地址
https://github.com/MrCoderStack/SpringBootDemo/tree/master/sb-xssfilter