一、XSS攻击的危害

**XSS攻击**通常值的是利用网页开发留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括JavaVBScriptActiveXFlash或者是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和Cookie等内容。
例如用户在发帖或者注册的时候,在文本框中输入<script>alert('xss')</script>,如果这段代码不经过转义处理,而是直接保存到数据库中。将来在视图层渲染HTML时,将这段代码输出到页面上,那么<script>标签的内容就会被执行。
通常情况下,我们登录到某个网站。如果网站使用HttpSession保存登录凭证,那么SessionId会以Cookie的形式保存在浏览器上。如果黑客在这个网页发帖的时候,填写的JavaScript代码是用来获取Cookie的内容,并且把Cookie内容通过Ajax发送给黑客自己的电脑。只要有人在这个网站上浏览黑客发的帖子,那么视图层渲染HTML页面,就会执行注入的XSS脚本,于是你的Cookie信息就泄漏了。黑客在自己的电脑上构建出Cookie,就可以冒充已经登录的用户。
即便很多网站使用了JWT,登陆凭证(Token令牌)是存储在浏览器上面的。所以用XSS脚本就可以轻松的从Storage中提取出Token,黑客依然可以轻松的冒充已经登录的用户。
所以避免XSS攻击最有效的办法就是对用户输入的数据进行转义,然后存储到数据库里面。等到视图层渲染HTML页面的时候,转以后的文字是不会被当做JavaScript执行的,这样就可以抵御XSS攻击

二、导入依赖包

Hutool工具包带有XSS转义的工具类,方便我们操作,所以导入之。

  1. <dependency>
  2. <groupId>cn.hutool</groupId>
  3. <artifactId>hutool-all</artifactId>
  4. <version>5.6.3</version>
  5. </dependency>

三、实现功能

我们从接口中获取到数据,都会经过HttpServletRequest,这是一个接口。如果我们想要重新定义请求类,不应该是扩展这个接口。因为HttpServletRequest接口中的抽象方法太多了,逐一实现太耗费时间。所以要挑选一个简单的自定义请求类的方式,就是继承HttpServletRequestWrapper类。
JavaEE 只是一个标准,具体的实现由各家应用服务厂商来完成。比如说Tomcat在实现
Servlet规范的时候,就自定义了HttpServletRequest接口的实现类。同时 JavaEE 规范还定义了
HttpServletRequestWrapper,这个类是请求类的包装类,用上了装饰器模式。所以无论各家应用服务器厂商怎么去实现HttpServletRequest接口,用户想要自定义请求,只需要继承
HttpServletRequestWrapper类并覆写某个方法即可。然后把请求传入请求包装类,装饰器模式就会替代请求对象中对应的某个方法。用户的代码和服务器厂商的代码完全解耦,我们就不需要关心
HttpServletRequest接口是怎么实现的,借助于包装类我们可以随意修改请求中的方法。

3.1 实现接口参数转义

  1. import cn.hutool.core.util.StrUtil;
  2. import cn.hutool.http.HtmlUtil;
  3. import cn.hutool.json.JSONUtil;
  4. import javax.servlet.ReadListener;
  5. import javax.servlet.ServletInputStream;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletRequestWrapper;
  8. import java.io.*;
  9. import java.nio.charset.StandardCharsets;
  10. import java.util.LinkedHashMap;
  11. import java.util.Map;
  12. /**
  13. * 这里整体的实现思路:
  14. * - 就是获取请求中的参数,判断是否是String类型的数据,如果是String类型的数据,就经过转义
  15. * 转义成没有<script>的普通文本。
  16. * - 然后再将 Wrapper 重新添加到 HttpFilter 中。
  17. */
  18. public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
  19. public XssHttpServletRequestWrapper(HttpServletRequest request) {
  20. super(request);
  21. }
  22. @Override
  23. public String getParameter(String name) {
  24. String value = super.getParameter(name);
  25. if (StrUtil.isNotBlank(value)) {
  26. value = HtmlUtil.filter(value);
  27. }
  28. return value;
  29. }
  30. @Override
  31. public String[] getParameterValues(String name) {
  32. String[] values = super.getParameterValues(name);
  33. if (values != null) {
  34. for (int i = 0; i < values.length; i++) {
  35. String child = values[i];
  36. if (StrUtil.isNotBlank(child)) {
  37. values[i] = HtmlUtil.filter(child);
  38. }
  39. }
  40. }
  41. return values;
  42. }
  43. @Override
  44. public Map<String, String[]> getParameterMap() {
  45. Map<String, String[]> map = super.getParameterMap();
  46. Map<String, String[]> result = new LinkedHashMap<>();
  47. for (String key : map.keySet()) {
  48. String[] values = map.get(key);
  49. if (values != null) {
  50. for (int i = 0; i < values.length; i++) {
  51. String child = values[i];
  52. if (StrUtil.isNotBlank(child)) {
  53. values[i] = HtmlUtil.filter(child);
  54. }
  55. }
  56. }
  57. result.put(key, values);
  58. }
  59. return result;
  60. }
  61. @Override
  62. public String getHeader(String name) {
  63. String value = super.getHeader(name);
  64. if (StrUtil.isNotBlank(value)) {
  65. value = HtmlUtil.filter(value);
  66. }
  67. return value;
  68. }
  69. @Override
  70. public ServletInputStream getInputStream() throws IOException {
  71. InputStream params = super.getInputStream();
  72. InputStreamReader isReader = new InputStreamReader(params, StandardCharsets.UTF_8);
  73. BufferedReader reader = new BufferedReader(isReader);
  74. String line = reader.readLine();
  75. StringBuffer buffer = new StringBuffer();
  76. while (line != null) {
  77. buffer.append(line);
  78. line = reader.readLine();
  79. }
  80. reader.close();
  81. isReader.close();
  82. params.close();
  83. Map<String, Object> map = JSONUtil.parseObj(buffer);
  84. Map<String, Object> result = new LinkedHashMap<>(map.size());
  85. for (String key : map.keySet()) {
  86. Object val = map.get(key);
  87. if (val instanceof String) {
  88. result.put(key, HtmlUtil.filter(val.toString()));
  89. } else {
  90. result.put(key, val);
  91. }
  92. }
  93. String json = JSONUtil.toJsonStr(result);
  94. ByteArrayInputStream array = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
  95. return new ServletInputStream() {
  96. @Override
  97. public boolean isFinished() {
  98. return false;
  99. }
  100. @Override
  101. public boolean isReady() {
  102. return false;
  103. }
  104. @Override
  105. public void setReadListener(ReadListener listener) {
  106. }
  107. @Override
  108. public int read() throws IOException {
  109. return array.read();
  110. }
  111. };
  112. }
  113. }

3.2 将转义后的请求重新添加到请求链中

  1. // 【urlPatterns = "/*"】表示过滤所有请求
  2. @WebFilter(urlPatterns = "/*")
  3. public class XssFilter extends HttpFilter {
  4. @Override
  5. protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
  6. XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(request);
  7. chain.doFilter(wrapper, response);
  8. }
  9. }

3.3 注册 XssFilter 过滤器

在【3.2】中,注意到XssFilter类上添加@WebFilter注解,如果想要该注解生效,则必须在启动类上添加@ServletComponentScan注解。

  1. @SpringBootApplication
  2. @ServletComponentScan
  3. public class MyApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(MyApplication.class, args);
  6. }
  7. }

在 SpringBootApplicaiton 上使用 @ServletComponentScan 注解的作用如下:

  • Servlet可以直接通过@WebServlet注解自动注册;
  • Filter可以直接通过@WebFilter注解自动注册;
  • Listener可以直接通过@WebListener注解自动注册;

3.4 测试功能

新建如下接口:

  1. @ApiOperation("登录接口")
  2. @GetMapping("/login")
  3. public String login(String name) {
  4. return name;
  5. }

结果如下图:<script>alert(100)</script>经过过滤变成了alert(100)
image.png