目录
一、XSS 攻击的危害
什么是 XSS 攻击? 跨站脚本攻击(XSS,Cross-Site Scripting)是指攻击者利用网页的漏洞,将恶意代码注入到网页中,从而在其他用户的浏览器中执行这些恶意代码。
XSS 攻击的危害包括:
-
窃取用户信息:例如会话
cookie
、登录凭证(如JWT
Token)等。 -
伪造身份:攻击者利用窃取的
cookie
或 Token 冒充用户执行操作。 -
劫持用户操作:在用户不知情的情况下执行敏感操作,如转账、修改密码。
-
网页篡改:动态修改网页内容,展示恶意广告等。
案例分析:
用户在某个网站发帖时输入以下代码:
<script>alert('1234')</script>
如果服务器直接将该内容存储到数据库,且在页面渲染时未做任何过滤处理,浏览器就会直接执行这段代码。这不仅会触发弹窗,还可能执行恶意的 JavaScript,如窃取 cookie
或令牌并发送到攻击者的服务器。
二、导入依赖库
因为 Hutool 工具包带有XSS转义的工具类,所以我们要导入Hutool,然后利用 Servlet 规范提供的请求包装类,定义数据转义功能。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>
三、定义请求包装类
在 Web 项目中,`HttpServletRequest` 是一个接口,如果我们想自定义请求类,直接实现这个接口并不实际。因为接口中有大量抽象方法需要实现,过程繁琐且耗时。更简便的方式是继承 `HttpServletRequestWrapper` 类。
`HttpServletRequestWrapper` 是 JavaEE 规范中定义的请求包装类,采用了装饰器模式。无论各个应用服务器(如 Tomcat)如何实现 `HttpServletRequest` 接口,用户只需继承 `HttpServletRequestWrapper`,覆盖所需方法即可。通过包装类,用户可以灵活地修改请求方法,而无需关心接口的具体实现。这种方式有效解耦了用户代码与服务器实现代码,既简化了操作,又增强了代码的扩展性。
package com.example.emos.wx.config.xss;
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.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
// 自定义 HttpServletRequestWrapper 用于 XSS 过滤
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
// 构造函数,接收 HttpServletRequest 对象
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* 重写 getParameter 方法,过滤单个请求参数的值
* @param name 参数名
* @return 过滤后的参数值
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name); // 调用父类方法获取参数值
if (value != null) {
value = HtmlUtil.filter(value); // 使用 Hutool 工具类进行 XSS 过滤
}
return value;
}
/**
* 重写 getParameterValues 方法,过滤数组形式的请求参数值
* @param name 参数名
* @return 过滤后的参数值数组
*/
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name); // 调用父类方法获取参数值数组
if (values != null) {
for (int i = 0; i < values.length; i++) {
values[i] = HtmlUtil.filter(values[i]); // 逐一过滤参数值
}
}
return values;
}
/**
* 重写 getParameterMap 方法,过滤请求参数的键值对
* @return 过滤后的参数映射
*/
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> parameters = super.getParameterMap(); // 获取原始参数映射
Map<String, String[]> map = new LinkedHashMap<>(); // 用 LinkedHashMap 保证顺序
if (parameters != null) {
for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
String key = entry.getKey(); // 获取参数名
String[] values = entry.getValue(); // 获取参数值数组
if (values != null) {
for (int i = 0; i < values.length; i++) {
if (values[i] != null && !StrUtil.hasEmpty(values[i])) {
// 过滤非空参数值
values[i] = HtmlUtil.filter(values[i]);
}
}
}
map.put(key, values); // 放入过滤后的映射
}
}
return map;
}
/**
* 重写 getHeader 方法,过滤请求头的值
* @param name 请求头名
* @return 过滤后的请求头值
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name); // 调用父类方法获取请求头值
if (value != null) {
value = HtmlUtil.filter(value); // 过滤请求头值
}
return value;
}
/**
* 重写 getInputStream 方法,处理请求体的内容
* @return 包含过滤后内容的 ServletInputStream
* @throws IOException 异常
*/
@Override
public ServletInputStream getInputStream() throws IOException {
// 获取原始请求输入流
ServletInputStream inputStream = super.getInputStream();
// 读取输入流内容并存入 StringBuffer
StringBuffer body = new StringBuffer();
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(reader);
String line = bufferedReader.readLine();
while (line != null) {
body.append(line); // 将每行追加到 body 中
line = bufferedReader.readLine();
}
// 关闭流以释放资源
reader.close();
bufferedReader.close();
inputStream.close();
// 将请求体内容解析为 Map 对象
Map<String, Object> map = JSONUtil.parseObj(body.toString());
Map<String, Object> resultMap = new HashMap<>(map.size());
for (String key : map.keySet()) {
Object val = map.get(key);
if (val instanceof String) {
// 对 String 类型的值进行 XSS 过滤
resultMap.put(key, HtmlUtil.filter((String) val));
} else {
// 非 String 类型直接放入
resultMap.put(key, val);
}
}
// 将处理后的 Map 转回 JSON 字符串
String jsonStr = JSONUtil.toJsonStr(resultMap);
// 构造新的输入流以返回
final ByteArrayInputStream bain = new ByteArrayInputStream(jsonStr.getBytes());
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bain.read(); // 从字节数组流中读取数据
}
@Override
public boolean isFinished() {
return false; // 表示未完成(可以根据实际需要调整逻辑)
}
@Override
public boolean isReady() {
return false; // 表示未准备好(可以根据实际需要调整逻辑)
}
@Override
public void setReadListener(ReadListener readListener) {
// 空实现
}
};
}
}
四、创建过滤器,把所有请求对象传入包装类
package com.example.emos.wx.config.xss;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Xss 是一个过滤器类,用于防止 XSS 攻击。
* 通过使用自定义的 HttpServletRequestWrapper(XssHttpServletRequestWrapper)
* 对请求内容进行过滤。
*/
@WebFilter(urlPatterns = "/*") // 声明过滤器,应用于所有 URL
public class Xss implements Filter {
/**
* 初始化方法,当过滤器实例化时调用。
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 调用父类的 init 方法(可选操作)
Filter.super.init(filterConfig);
}
/**
* 核心过滤逻辑:将所有 HTTP 请求包装为 XssHttpServletRequestWrapper,
* 并将其传递到过滤链的下一环节。
* @param servletRequest 原始请求对象
* @param servletResponse 原始响应对象
* @param filterChain 过滤器链
* @throws IOException IO 异常
* @throws ServletException Servlet 异常
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
// 将原始 ServletRequest 强制转换为 HttpServletRequest
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 使用自定义的 XssHttpServletRequestWrapper 包装请求
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);
// 将包装后的请求传递到过滤链的下一环节
filterChain.doFilter(xssRequest, servletResponse);
}
/**
* 销毁方法,当过滤器被卸载时调用。
*/
@Override
public void destroy() {
// 调用父类的 destroy 方法(可选操作)
Filter.super.destroy();
}
}
五、给主类添加注解
给SpringBoot主类添加
@ServletComponentScan
注解。
六、测试拦截XSS脚本
1.在Swagger中,执行 sayHello()方法,向name属性传入<script>HelloWorld</script>,然后观察返回的结果。