【SpringBoot应用篇】SpringBoot集成AntiSamy防御XSS(跨站脚本攻击)--过滤器实现
XSS介绍
XSS:跨站脚本攻击(Cross Site Scripting),为不和 CSS混淆,故将跨站脚本攻击缩写为XSS。XSS是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。有点类似于sql注入。
XSS攻击原理:
HTML是一种超文本标记语言,通过将一些字符特殊地对待来区别文本和标记,例如,小于符号(<)被看作是HTML标签的开始,
AntiSamy介绍
AntiSamy是OWASP的一个开源项目,通过对用户输入的 HTML / CSS / JavaScript 等内容进行检验和清理,确保输入符合应用规范。AntiSamy被广泛应用于Web服务对存储型和反射型XSS的防御中。
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>1.5.7</version>
</dependency>
AntiSamy使用
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>1.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
User
@Data
public class User {
private int id;
private String name;
private int age;
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/save")
public String save(User user){
System.out.println("UserController save.... " + user);
return user.getName();
}
}
此时我们可以启动项目进行访问,但是还没有进行参数的过滤,所以如果我们输入任意参数都可以正常传递到Controller中,这在实际项目中是非常不安全的。为了对我们输入的数据进行过滤清理,需要通过过滤器来实现。
XssFilter
创建过滤器,用于过滤所有提交到服务器的请求参数
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
//传入重写后的Request
filterChain.doFilter(new XssRequestWrapper(request),servletResponse);
}
}
注意:通过上面的过滤器可以发现我们并没有在过滤器中直接进行请求参数的过滤清理,而是直接放行了,那么我们还怎么进行请求参数的过滤清理呢?其实过滤清理的工作是在另外一个类XssRequestWrapper中进行的,当上面的过滤器放行时需要调用filterChain.doFilter()方法,此方法需要传入请求Request对象,此时我们可以将当前的request对象进行包装,而XssRequestWrapper就是Request对象的包装类,在过滤器放行时会自动调用包装类的getParameterValues方法,我们可以在包装类的getParameterValues方法中进行统一的请求参数过滤清理。
XssRequestWrapper
1、jar包自带了策略文件,选择antisamy-ebay.xml复制到resouce目录下
public class XssRequestWrapper extends HttpServletRequestWrapper {
/**
* 策略文件 需要将要使用的策略文件放到项目资源文件路径下
* */
private static String antiSamyPath = XssRequestWrapper.class.getClassLoader().getResource( "antisamy-ebay.xml").getFile();
public static Policy policy = null;
static {
// 指定策略文件
try {
policy = Policy.getInstance(antiSamyPath);
} catch (PolicyException e) {
e.printStackTrace();
}
}
/**
* AntiSamy过滤数据
* @param taintedHTML 需要进行过滤的数据
* @return 返回过滤后的数据
* */
private String xssClean( String taintedHTML){
try{
// 使用AntiSamy进行过滤
AntiSamy antiSamy = new AntiSamy();
CleanResults cr = antiSamy.scan( taintedHTML, policy);
taintedHTML = cr.getCleanHTML();
}catch( ScanException e) {
e.printStackTrace();
}catch( PolicyException e) {
e.printStackTrace();
}
return taintedHTML;
}
public XssRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String[] getParameterValues(String name){
String[] values = super.getParameterValues(name);
if ( values == null){
return null;
}
int len = values.length;
String[] newArray = new String[len];
for (int j = 0; j < len; j++){
// 过滤清理
newArray[j] = xssClean(values[j]);
}
return newArray;
}
@Override
public String getParameter(String paramString) {
String str = super.getParameter(paramString);
if (str == null) {
return null;
}
return xssClean(str);
}
@Override
public String getHeader(String paramString) {
String str = super.getHeader(paramString);
if (str == null) {
return null;
}
return xssClean(str);
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> requestMap = super.getParameterMap();
for (Map.Entry<String, String[]> me : requestMap.entrySet()) {
String[] values = me.getValue();
for (int i = 0; i < values.length; i++) {
values[i] = xssClean(values[i]);
}
}
return requestMap;
}
}
配置类
为了使上面定义的过滤器生效,需要创建配置类,用于初始化过滤器对象
@Configuration
public class AntiSamyConfiguration {
/**
* 配置跨站攻击过滤器
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean(new XssFilter());
filterRegistration.addUrlPatterns("/*");
filterRegistration.setOrder(1);
Map<String, String> initParameters = new HashMap<>(2);
String excludes = new StringJoiner(",")
.add("/favicon.ico")
.add("/doc.html")
.add("/swagger-ui.html")
.add("/csrf")
.add("/webjars/*")
.add("/v2/*")
.add("/swagger-resources/*")
.add("/resources/*")
.add("/static/*")
.add("/public/*")
.add("/classpath:*")
.add("/actuator/*")
.toString();
initParameters.put("excludes", excludes);
initParameters.put("isIncludeRichText", "true");
filterRegistration.setInitParameters(initParameters);
return filterRegistration;
}
}
正则表达式解决XSS和SQL注入安全问题
pom
<!--hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
XssAndSqlFilter 过滤器
@Component
@ConfigurationProperties(prefix = "security")
@WebFilter(urlPatterns = "/*",asyncSupported = true)
@Order(1)
public class XssAndSqlFilter implements Filter {
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
/**
* 是否启用
*/
private boolean enable;
/**
* 忽略的URL
*/
private List<String> excludes;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//跳过不需要的Xss校验的地址
// 不启用或者已忽略的URL不拦截
if (!enable || isExcludeUrl(req.getServletPath())) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//注入xss过滤器实例
XssAndSqlHttpServletRequestWrapper reqW = new XssAndSqlHttpServletRequestWrapper(req);
try {
//过滤
filterChain.doFilter(reqW, response);
} catch (Exception e) {
resolver.resolveException(req, response, null, e);
}
}
/**
* 判断是否为忽略的URL
*
* @param url URL路径
* @return true-忽略,false-过滤
*/
private boolean isExcludeUrl(String url) {
if (excludes == null || excludes.isEmpty()) {
return false;
}
return excludes.stream().map(pattern -> Pattern.compile("^" + pattern)).map(p -> p.matcher(url)).anyMatch(Matcher::find);
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public void setExcludes(List<String> excludes) {
this.excludes = excludes;
}
}
XssAndSqlHttpServletRequestWrapper
public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger log = LoggerFactory.getLogger(XssAndSqlHttpServletRequestWrapper.class);
/**
* post请求体
*/
private byte[] body;
/**
* 是否是文件上传
*/
private boolean fileUpload = false;
/**
* sql注入正则
*/
private static String badStrReg =
"\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
/**
* xss脚本正则
*/
private final static Pattern[] scriptPatterns = {
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
};
/**
* 构造器,将request传递给父类
* @param request
*/
public XssAndSqlHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* 获取单个参数,将参数进行转义
* @param name
* @return
*/
@Override
public String getParameter(String name) {
//获取参数
String value = super.getParameter(name);
if(!StrUtil.hasBlank(value)){
//对参数进行转义
value = cleanXSS(cleanSQLInject(value));
}
return value;
}
/**
* 获取多个参数进行转义,返回值为数组
* @param name
* @return
*/
@Override
public String[] getParameterValues(String name) {
//获取集合
String[] values = super.getParameterValues(name);
//判断集合是否为空,如果不为空,进行转义
if(values != null){
//遍历数组
for(int i = 0;i < values.length;i++){
String value = values[i];
if(!StrUtil.hasBlank(value)){
//转义
value = cleanXSS(cleanSQLInject(value));
}
//将转义后的数据放回数组
values[i] = value;
}
}
return values;
}
/**
* 获取请求头的数据,并进行转义
* @param name
* @return
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if(!StrUtil.hasBlank(value)){
value = cleanXSS(cleanSQLInject(value));
}
return value;
}
/**
* 获取Map参数
* @return
*/
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> parameterMap = super.getParameterMap();
//因为super.getParameterMap()返回的是Map,所以我们需要定义Map的实现类对数据进行封装
Map<String,String[]> params = new LinkedHashMap<>();
//如果参数不为空
if(parameterMap != null){
//对map进行遍历
for(String key:parameterMap.keySet()){
//根据key获取value
String[] values = parameterMap.get(key);
//遍历数组
for(int i = 0;i<values.length;i++){
String value = values[i];
if(!StrUtil.hasBlank(value)){
//转义
value = cleanXSS(cleanSQLInject(value));
}
//将转义后的数据放回数组中
values[i] = value;
}
//将转义后的数组put到linkMap当中
params.put(key,values);
}
}
return params;
}
/**
* 获取@RequestBody输入流参数
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
//获取输入流
ServletInputStream in = super.getInputStream();
//用于存储输入流
StringBuffer body = new StringBuffer();
InputStreamReader reader = new InputStreamReader(in, Charset.forName("UTF-8"));
BufferedReader bufferedReader = new BufferedReader(reader);
//按行读取输入流
String line = bufferedReader.readLine();
while(line != null){
//将获取到的第一行数据append到StringBuffer中
body.append(line);
//继续读取下一行流,直到line为空
line = bufferedReader.readLine();
}
//关闭流
bufferedReader.close();
reader.close();
in.close();
//将body转换为map
Map<String,Object> map = JSONUtil.parseObj(body.toString());
//创建空的map用于存储结果
Map<String,Object> resultMap = new HashMap<>(map.size());
//遍历数组
for(String key:map.keySet()){
Object value = map.get(key);
//如果map.get(key)获取到的是字符串就需要进行转义,如果不是直接存储resultMap
if(map.get(key) instanceof String){
String str = cleanXSS(cleanSQLInject(value.toString()));
resultMap.put(key,str);
}else{
resultMap.put(key,value);
}
}
//将resultMap转换为json字符串
String resultStr = JSONUtil.toJsonStr(resultMap);
//将json字符串转换为字节
final ByteArrayInputStream bis = new ByteArrayInputStream(resultStr.getBytes());
//实现接口
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 bis.read();
}
};
}
/**
* 清除xss
* @param src 单个参数
* @return
*/
public String cleanXSS(String src) {
String temp = src;
// 校验xss脚本
for (Pattern pattern : scriptPatterns) {
temp = pattern.matcher(temp).replaceAll("");
}
// 校验xss特殊字符
temp = temp.replaceAll("\0|\n|\r", "");
temp = temp.replaceAll("<", "<").replaceAll(">", ">");
if (!temp.equals(src)) {
log.error("xss攻击检查:参数含有非法攻击字符,已禁止继续访问!!");
log.error("原始输入信息-->" + src);
throw new RuntimeException("xss攻击检查:参数含有非法攻击字符,已禁止继续访问!!");
}
return src;
}
/**
* 过滤sql注入 -- 需要增加通配,过滤大小写组合
* @param src 单个参数值
* @return
*/
public String cleanSQLInject(String src) {
// 非法sql注入正则
Pattern sqlPattern = Pattern.compile(badStrReg, Pattern.CASE_INSENSITIVE);
if (sqlPattern.matcher(src.toLowerCase()).find()) {
log.error("sql注入检查:输入信息存在SQL攻击!");
throw new RuntimeException("sql注入检查:参数含有非法攻击字符,已禁止继续访问!!");
}
return src;
}
}
属性配置
# 信息安全,防止xxs跨站脚本攻击,SQL注入
security:
enable: true
excludes: