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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/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; @Data public 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 { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; //传入重写后的Request filterChain.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(java.net.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); } @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++) { 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; //配置跨站攻击过滤器 @Configuration public class AntiSamyConfiguration { @Bean public 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; @SpringBootApplication public class AntiSamyApp { public static void main(String[] args) { SpringApplication.run(AntiSamyApp.class, args); } }
第十步,输入测试数据,并观察后台打印结果
从上图可以看到,测试的 XSS 攻击代码直接被清理掉了,服务器返回给浏览器的结果中没有 XSS 代码,不会被浏览器执行,成功避免了 XSS 攻击。
升级一下:
之前,我们在进行请求参数过滤时只是在包装类的 getParameterValues 方法中进行了处理,真实项目中可能用户提交的数据在请求头中,也可能用户提交的是 json 数据,所以如果考虑所有情况,我们可以在包装类中的多个方法中都进行清理处理即可,在 XssRequestWrapper 实现类中新增以下几个方法:
@Override public 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; } @Override public 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; } @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++) { System.out.println("Antisamy 过滤清理,清理之前的参数值:"+values[i]); //过滤清理 values[i] = xssClean(values[i]); System.out.println("Antisamy 过滤清理,清理之后的参数值:"+values[i]); } } return requestMap; }