一、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);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (StrUtil.isNotBlank(value)) {
value = HtmlUtil.filter(value);
}
return value;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (StrUtil.isNotBlank(value)) {
value = HtmlUtil.filter(value);
}
return value;
}
@Override
public 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() {
@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 array.read();
}
};
}
}
3.2 将转义后的请求重新添加到请求链中
// 【urlPatterns = "/*"】表示过滤所有请求
@WebFilter(urlPatterns = "/*")
public class XssFilter extends HttpFilter {
@Override
protected 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
@ServletComponentScan
public 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)
。