一、XSS攻击的危害
**XSS攻击**通常值的是利用网页开发留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、VBScript、ActiveX、Flash或者是普通的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转义的工具类,方便我们操作,所以导入之。
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.6.3</version></dependency>
三、实现功能
我们从接口中获取到数据,都会经过HttpServletRequest,这是一个接口。如果我们想要重新定义请求类,不应该是扩展这个接口。因为HttpServletRequest接口中的抽象方法太多了,逐一实现太耗费时间。所以要挑选一个简单的自定义请求类的方式,就是继承HttpServletRequestWrapper类。
JavaEE 只是一个标准,具体的实现由各家应用服务厂商来完成。比如说Tomcat在实现Servlet规范的时候,就自定义了HttpServletRequest接口的实现类。同时 JavaEE 规范还定义了HttpServletRequestWrapper,这个类是请求类的包装类,用上了装饰器模式。所以无论各家应用服务器厂商怎么去实现HttpServletRequest接口,用户想要自定义请求,只需要继承HttpServletRequestWrapper类并覆写某个方法即可。然后把请求传入请求包装类,装饰器模式就会替代请求对象中对应的某个方法。用户的代码和服务器厂商的代码完全解耦,我们就不需要关心HttpServletRequest接口是怎么实现的,借助于包装类我们可以随意修改请求中的方法。
3.1 实现接口参数转义
import cn.hutool.core.util.StrUtil;import cn.hutool.http.HtmlUtil;import cn.hutool.json.JSONUtil;import javax.servlet.ReadListener;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import java.io.*;import java.nio.charset.StandardCharsets;import java.util.LinkedHashMap;import java.util.Map;/*** 这里整体的实现思路:* - 就是获取请求中的参数,判断是否是String类型的数据,如果是String类型的数据,就经过转义* 转义成没有<script>的普通文本。* - 然后再将 Wrapper 重新添加到 HttpFilter 中。*/public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {public XssHttpServletRequestWrapper(HttpServletRequest request) {super(request);}@Overridepublic String getParameter(String name) {String value = super.getParameter(name);if (StrUtil.isNotBlank(value)) {value = HtmlUtil.filter(value);}return value;}@Overridepublic String[] getParameterValues(String name) {String[] values = super.getParameterValues(name);if (values != null) {for (int i = 0; i < values.length; i++) {String child = values[i];if (StrUtil.isNotBlank(child)) {values[i] = HtmlUtil.filter(child);}}}return values;}@Overridepublic Map<String, String[]> getParameterMap() {Map<String, String[]> map = super.getParameterMap();Map<String, String[]> result = new LinkedHashMap<>();for (String key : map.keySet()) {String[] values = map.get(key);if (values != null) {for (int i = 0; i < values.length; i++) {String child = values[i];if (StrUtil.isNotBlank(child)) {values[i] = HtmlUtil.filter(child);}}}result.put(key, values);}return result;}@Overridepublic String getHeader(String name) {String value = super.getHeader(name);if (StrUtil.isNotBlank(value)) {value = HtmlUtil.filter(value);}return value;}@Overridepublic ServletInputStream getInputStream() throws IOException {InputStream params = super.getInputStream();InputStreamReader isReader = new InputStreamReader(params, StandardCharsets.UTF_8);BufferedReader reader = new BufferedReader(isReader);String line = reader.readLine();StringBuffer buffer = new StringBuffer();while (line != null) {buffer.append(line);line = reader.readLine();}reader.close();isReader.close();params.close();Map<String, Object> map = JSONUtil.parseObj(buffer);Map<String, Object> result = new LinkedHashMap<>(map.size());for (String key : map.keySet()) {Object val = map.get(key);if (val instanceof String) {result.put(key, HtmlUtil.filter(val.toString()));} else {result.put(key, val);}}String json = JSONUtil.toJsonStr(result);ByteArrayInputStream array = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() throws IOException {return array.read();}};}}
3.2 将转义后的请求重新添加到请求链中
// 【urlPatterns = "/*"】表示过滤所有请求@WebFilter(urlPatterns = "/*")public class XssFilter extends HttpFilter {@Overrideprotected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(request);chain.doFilter(wrapper, response);}}
3.3 注册 XssFilter 过滤器
在【3.2】中,注意到XssFilter类上添加@WebFilter注解,如果想要该注解生效,则必须在启动类上添加@ServletComponentScan注解。
@SpringBootApplication@ServletComponentScanpublic class MyApplication {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);}}
在 SpringBootApplicaiton 上使用 @ServletComponentScan 注解的作用如下:
Servlet可以直接通过@WebServlet注解自动注册;Filter可以直接通过@WebFilter注解自动注册;Listener可以直接通过@WebListener注解自动注册;
3.4 测试功能
新建如下接口:
@ApiOperation("登录接口")@GetMapping("/login")public String login(String name) {return name;}
结果如下图:<script>alert(100)</script>经过过滤变成了alert(100)。
