AntiSamy:防 XSS 攻击的一种解决方案使用教程
1. XSS 介绍2. AntiSamy 介绍3. AntiSamy 使用3.1 导入依赖3.2 选择策略文件3.3 SpringBoot 整合 AntiSamy 使用1. XSS 介绍
XSS 是跨站脚本攻击(Cross Site Scripting) 的简称,为不和 CSS(Cascading Style Sheets) 混淆,故将跨站脚本攻击缩写为 XSS. XSS 是指恶意攻击者往 Web 页面里插入恶意 Script 代码,当用户浏览该页时,嵌入其中 Web 里面的 Script 代码会被执行,从而达到恶意攻击用户的目的。有点类似于 SQL 注入。当网站攻击者发现这个漏洞,并攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和 cookie 等各种内容。
XSS 攻击分为两种类型:
持久型:XSS 攻击代码被存储到服务器的数据库中,隐秘性很高。例如,当攻击者在评论或留言板注入 XSS 攻击代码,而帖子或博客被服务器存储下来,帖子的评论或留言板自然也就被持久化到服务器的数据库中,这里面就包含了 XSS 攻击代码。当其他用户浏览这个帖子的时候,XSS 攻击代码便开始在用户的浏览器中解析并执行。反射型:反射型 XSS 又称为非持久型 XSS,这种攻击方式具有一次性的特点。例如,攻击者将包含 XSS 代码的恶意链接发送给用户,当用户访问链接时,服务器收到用户请求并进行处理,再将包含 XSS 代码的数据返回给用户的浏览器,那么用户浏览器解析包含 XSS 代码的数据时,就会触发 XSS 漏洞。DOM 型:区别于以上两种类型,DOM 型 XSS 攻击不经过服务器,它是由攻击者直接构造一个包含 XSS 攻击代码的 URL,然后让目标用户去访问这个 URL,用户的浏览器在处理这个响应的时候,DOM 型对象就会处理 XSS 代码,触发 XSS 漏洞。
2. AntiSamy 介绍
因此为了避免这个漏洞给网站的用户带来的危害,OWASP 组织开源了一个叫做 AntiSamy 的项目,帮助我们的网站防御 XSS 攻击。它通过对用户输入的 HTML / CSS / JavaScript 等内容进行检验和清理,确保输入符合应用规范。AntiSamy 被广泛应用于 Web 服务对存储型和反射型 XSS 的防御中。
官方给出的关于 AntiSamy 的介绍是这样的:
AntiSamy 是一个 API 或 库 ,可以帮助我们开发者确保客户端不会在他们提供的 HTML 中提供恶意的代码,这些 HTML 用于保存在服务器上的配置文件、注释等。关于 web 应用程序的术语“恶意代码”通常指“JavaScript”。大多数情况下,CSS 只有在调用 JavaScript 时才被认为是恶意的。然而,在许多情况下,“正常的” HTML 和 CSS 可以被恶意使用。
3. AntiSamy 使用
3.1 导入依赖
AntiSamy 的 maven 坐标:
<dependency><groupId>org.owasp.antisamy</groupId><artifactId>antisamy</artifactId><version>1.6.2</version></dependency>
3.2 选择策略文件
AntiSamy 预定义了一些策略文件,这些策略文件它们代表了允许用户提供 HTML (可能还有CSS) 格式化信息的典型应用场景,我们可以根据自己的应用场景选择合适的策略文件。具体的策略文件有以下几种:
1、antisamy-slashdot.xml
Slashdot 是一个技术新闻网站,它允许用户匿名回复非常有限的 HTML 标记的新闻帖子。现在,Slashdot 不仅是最酷的网站之一,它也是一个受到许多不同成功攻击的网站。Slashdot 的规则相当严格:用户只能提交以下<b>, <u>, <i>, <a>, <blockquote>
这些 HTML 标记,不能提交 CSS.因此,antisamy-slashdot.xml 文件支持类似的功能,所有直接对字体、颜色或重点进行操作的文本格式标记都是允许的,但是不允许 CSS 和 JavaScript 标记出现。
2、antisamy-ebay.xml
eBay 是世界上最受欢迎的在线拍卖网站,它是一个公共站点,因此任何人都可以发布包含丰富 HTML 内容的清单。考虑到 eBay 作为一个有吸引力的目标,它受到一些复杂的 XSS 攻击并不奇怪。清单被允许包含比 Slashdot 更丰富的内容——所以它的攻击面相当大。因此,antisamy-ebay.xml 策略文件提供的策略是支持丰富的 HTML 标记,但是不支持 CSS 标记 和 JavaScript 标记。
3、antisamy-myspace.xml
MySpace 是一个曾经非常受欢迎的社交网站,用户可以提交几乎所有他们想要的 HTML 和CSS ——只要不包含 JavaScript. MySpace 使用一个单词黑名单来验证用户的 HTML,这就是为什么他们会受到臭名昭著的 Samy 蠕虫的攻击。Samy 蠕虫使用碎片攻击和一个应该被列入黑名单的词(eval)——是这个项目的灵感来源。因此,antisamy-myspace.xml 策略文件提供的策略是支持非常丰富的 HTML 和 CSS 标记,但是不支持 JavaScript 标记。
4、antisamy-anythinggoes.xml
如果想允许每一个有效的 HTML 和 CSS 元素(但是不允许 JavaScript 或明显的 CSS 相关的钓鱼攻击),你可以使用这个策略文件。它包含每个元素的基本规则,所以在使用定制其他策略文件时,可以将它用作知识库。
5、antisamy-tinymce.xml
只允许文本格式通过,相对比较安全。
6、antisamy.xml
默认规则,允许大部分 HTML 标记,不允许 JavaScript 标记出现。
3.3 SpringBoot 整合 AntiSamy 使用
工程的目录结构如下:
第一步,创建 maven 工程 antiSamy_demo 并配置 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.hzz</groupId><artifactId>antiSamy_demo</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version><relativePath/></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.owasp.antisamy</groupId><artifactId>antisamy</artifactId><version>1.6.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies></project>
第二步,创建 application.yml 文件
server:port: 9000
第三步,创建策略文件 /resources/antisamy-slashdot.xml,策略文件可以直接从 antisamy jar 包下复制
第四步,创建实体类 User
package com.hzz.entity;import lombok.Data;@Datapublic class User {private int id;private String name;private int age;}
第五步,创建 UserController
package com.hzz.controller;import com.hzz.entity.User;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/user")public class UserController {@RequestMapping("/save")public String save(User user){System.out.println("UserController save.... " + user);return user.getName();}}
第六步,创建 /resources/static/index.html 页面
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title></head><body><form method="post" action="/user/save">id:<input type="text" name="id"><br>name:<input type="text" name="name"><br>age:<input type="text" name="age"><br><input type="submit" value="submit"></form></body></html>
第七步,创建过滤器,用于过滤所有提交到服务器的请求参数
package com.hzz.filter;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;//过滤所有提交到服务器的请求参数public class XssFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;//传入重写后的RequestfilterChain.doFilter(new XssRequestWrapper(request), servletResponse);}}
过滤器 XssFilter 并没有直接进行请求参数的过滤清理,而是直接放行。其实,过滤清理的工作是在另外一个类 XssRequestWrapper 中进行的,当上面的过滤器放行时需要调用filterChain.doFilter() 方法,此方法需要传入请求 request 对象,此时我们可以将当前的 request 对象进行包装,而 XssRequestWrapper 就是 request 对象的包装类,在过滤器放行时会自动调用包装类的 getParameterValues 方法,我们可以在包装类的 getParameterValues 方法中进行统一的请求参数过滤清理。
XssRequestWrapper 包装类实现如下
package com.hzz.filter;import org.owasp.validator.html.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import java.io.UnsupportedEncodingException;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(.URLDecoder.decode(antiSamyPath, "utf-8")); //我的项目路径带有中文,需要转码,否则会报错,如果你的路径都是英文则忽略转码过程} catch (PolicyException | UnsupportedEncodingException 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);}@Overridepublic 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++) {System.out.println("Antisamy 过滤清理,清理之前的参数值:"+values[j]);//过滤清理newArray[j] = xssClean(values[j]);System.out.println("Antisamy 过滤清理,清理之后的参数值:"+newArray[j]);}return newArray;}}
第八步,为了使上面定义的过滤器生效,需要创建配置类,用于初始化过滤器对象
package com.hzz.config;import com.hzz.filter.XssFilter;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;//配置跨站攻击过滤器@Configurationpublic class AntiSamyConfiguration {@Beanpublic FilterRegistrationBean filterRegistrationBean() {FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new XssFilter());filterRegistrationBean.addUrlPatterns("/*");filterRegistrationBean.setOrder(1);return filterRegistrationBean;}}
第九步,创建启动类并启动项目,访问网站首页地址 http://localhost:9000/index.html
package com.hzz;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class AntiSamyApp {public static void main(String[] args) {SpringApplication.run(AntiSamyApp.class, args);}}
第十步,输入测试数据,并观察后台打印结果
从上图可以看到,测试的 XSS 攻击代码直接被清理掉了,服务器返回给浏览器的结果中没有 XSS 代码,不会被浏览器执行,成功避免了 XSS 攻击。
升级一下:
之前,我们在进行请求参数过滤时只是在包装类的 getParameterValues 方法中进行了处理,真实项目中可能用户提交的数据在请求头中,也可能用户提交的是 json 数据,所以如果考虑所有情况,我们可以在包装类中的多个方法中都进行清理处理即可,在 XssRequestWrapper 实现类中新增以下几个方法:
@Overridepublic String getParameter(String paramString) {String str = super.getParameter(paramString);if (str == null) {return null;}System.out.println("Antisamy 过滤清理,清理之前的参数值:"+str);//过滤清理str = xssClean(str);System.out.println("Antisamy 过滤清理,清理之后的参数值:"+str);return str;}@Overridepublic String getHeader(String paramString) {String str = super.getHeader(paramString);if (str == null) {return null;}System.out.println("Antisamy 过滤清理,清理之前的参数值:"+str);//过滤清理str = xssClean(str);System.out.println("Antisamy 过滤清理,清理之后的参数值:"+str);return str;}@Overridepublic 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++) {System.out.println("Antisamy 过滤清理,清理之前的参数值:"+values[i]);//过滤清理values[i] = xssClean(values[i]);System.out.println("Antisamy 过滤清理,清理之后的参数值:"+values[i]);}}return requestMap;}