前端安全

深入理解前端安全:XSS、CSRF、CSP、CORS、SQL 注入防护,以及 HTTPS、WebAuthn 等现代安全技术。


安全威胁概览

┌──────────────────────────────────────────────────────────────┐
│                   OWASP Top 10 (前端相关)                     │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Broken Access Control        访问控制失效                 │
│  2. Cryptographic Failures       加密失败                    │
│  3. Injection                    注入                        │
│  4. Insecure Design              不安全设计                  │
│  5. Security Misconfiguration     安全配置错误                │
│  6. Vulnerable Components        脆弱组件                    │
│  7. Authentication Failures      认证失效                    │
│  8. Integrity Failures           完整性失效                  │
│  9. Logging & Monitoring         日志与监控                  │
│  10. SSRF                        服务端请求伪造               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

XSS(跨站脚本)

XSS 原理

┌──────────────────────────────────────────────────────────────┐
│                      XSS 攻击原理                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   攻击者                                                   │
│      │                                                     │
│      │  在评论区提交:                                       │
│      │  <script>stealCookies()</script>                   │
│      ▼                                                     │
│   ┌─────────┐                                               │
│   │  服务器  │ ← 存储型 XSS                               │
│   └────┬────┘                                               │
│        │                                                    │
│        │  其他用户访问页面                                   │
│        ▼                                                    │
│   ┌─────────┐                                               │
│   │  浏览器  │ ← 脚本被执行,窃取敏感信息                   │
│   └─────────┘                                               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

XSS 类型

类型 说明 例子
存储型 恶意脚本永久存储在服务器 评论区注入脚本
反射型 恶意脚本通过 URL 参数注入 ?search=<script>
DOM 型 仅客户端执行,不涉及服务器 URL hash 注入

XSS 防护

// 1. HTML 转义
function escapeHtml(str) {
  const escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  };

  return str.replace(/[&<>"']/g, char => escapeMap[char]);
}

// 2. React 自动防护(默认不插入原始 HTML)
const userInput = '<script>alert(1)</script>';
// React 会自动转义
return <div>{userInput}</div>;  // 安全

// 3. 避免 dangerouslySetInnerHTML
// 危险方式
return <div dangerouslySetInnerHTML={{ __html: userHtml }} />;

// 安全方式:使用 DOMPurify
import DOMPurify from 'dompurify';

const sanitized = DOMPurify.sanitize(userHtml, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
  ALLOWED_ATTR: ['class']
});

return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;

Vue 防护

<!-- Vue 自动防护 -->
<template>
  <!-- 自动转义 -->
  <div>{{ userInput }}</div>

  <!-- 需要手动处理 -->
  <div v-html="sanitizedHtml"></div>
</template>

CSRF(跨站请求伪造)

CSRF 原理

┌──────────────────────────────────────────────────────────────┐
│                      CSRF 攻击原理                            │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   用户已登录 site-a.com                                     │
│         │                                                   │
│         ▼                                                   │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              site-a.com 页面                        │   │
│   │  <img src="site-b.com/transfer?to=hacker&amt=1000"> │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
│   浏览器自动携带 cookie,发起请求                             │
│         │                                                   │
│         ▼                                                   │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              site-b.com 服务器                       │   │
│   │  认为请求来自用户,执行转账操作                       │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

CSRF 防护

// 1. CSRF Token
// 服务器端生成并验证
const csrfToken = generateToken();  // 生成 token
session.csrfToken = csrfToken;

// 前端请求时携带 token
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ to: 'hacker', amount: 1000 })
});

// 服务器验证
function validateCsrfToken(req, res, next) {
  const token = req.headers['x-csrf-token'];
  if (token !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  next();
}

// 2. SameSite Cookie
// 设置 SameSite 属性
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly

// SameSite 选项:
// - Strict: 完全禁止跨站发送 cookie
// - Lax: GET 请求可发送,POST 不可
// - None: 允许跨站,但必须配合 Secure

// 3. 自定义请求头
// 浏览器安全限制:跨域请求无法设置自定义头
fetch('/api/action', {
  method: 'POST',
  headers: {
    'X-Requested-With': 'XMLHttpRequest'  // 自定义头
  }
});

框架集成

// axios 配置 CSRF Token
import axios from 'axios';

axios.interceptors.request.use(config => {
  const csrfToken = document.querySelector('meta[name=csrf-token]')?.content;
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken;
  }
  return config;
});

CSP(内容安全策略)

CSP 配置

# Nginx 配置 CSP
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'nonce-random123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
" always;
// Meta 标签
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'">

CSP 指令

指令 说明 示例
default-src 默认来源 'self'
script-src JS 来源 'self' 'nonce-random'
style-src CSS 来源 'self' 'unsafe-inline'
img-src 图片来源 'self' data: https:
connect-src XHR/Fetch 'self' https://api.example.com
font-src 字体来源 'self' https://fonts.gstatic.com
frame-ancestors 嵌入来源 'none'
base-uri 来源 'self'
form-action 表单提交 'self'

nonce 策略

// 服务器生成 nonce 并注入到页面
const nonce = crypto.randomBytes(16).toString('base64');

// CSP 头
// Content-Security-Policy: script-src 'nonce-{nonce}'

// HTML 中
<script nonce="random123">
  console.log('Script with nonce');
</script>

// React 中
<script
  nonce="random123"
  dangerouslySetInnerHTML={{ __html: 'console.log("inline")' }}
/>

CORS(跨域资源共享)

CORS 原理

┌──────────────────────────────────────────────────────────────┐
│                        CORS 原理                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   简单请求 (GET/POST, 标准头部)                               │
│   ┌────────┐         ┌────────┐                              │
│   │ 浏览器  │ ──────→ │ 服务器  │                              │
│   └────────┘  GET     └────────┘                              │
│   │                  │                                       │
│   │                  │ Access-Control-Allow-Origin           │
│   │←─────────────────│                                      │
│   │  响应            │                                      │
│                                                              │
│   预检请求 (PUT/DELETE, 自定义头)                             │
│   ┌────────┐         ┌────────┐                              │
│   │ 浏览器  │ ──OPTIONS──→ │ 服务器  │                          │
│   └────────┘         └────────┘                              │
│   │                  │                                       │
│   │  Access-Control-Allow-Origin                            │
│   │←─────────────────│                                      │
│   │  预检响应        │                                      │
│   │                  │                                       │
│   │ ────────────────→│  实际请求                             │
│   │                  │                                       │
│   │←──────────────────│  响应                                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

服务器端配置

// Express CORS 中间件
const cors = require('cors');

const corsOptions = {
  origin: 'https://example.com',  // 允许的来源
  // origin: ['https://a.com', 'https://b.com'],  // 多域名
  // origin: (origin, callback) => { ... },  // 动态判断
  credentials: true,  // 允许携带 cookie
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  exposedHeaders: ['X-Total-Count'],  // 允许前端访问的头
  maxAge: 86400  // 预检请求缓存时间
};

app.use(cors(corsOptions));

前端配置

// 携带凭证的请求
fetch('https://api.example.com/data', {
  credentials: 'include'  // 包含 cookie
});

// axios 配置
axios.create({
  withCredentials: true
});

// 简单请求示例
// GET /data HTTP/1.1
// Origin: https://example.com

// 响应
// HTTP/1.1 200 OK
// Access-Control-Allow-Origin: https://example.com
// Access-Control-Allow-Credentials: true

输入验证与过滤

客户端验证

// 邮箱验证
function isValidEmail(email) {
  const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return re.test(email);
}

// URL 验证
function isValidUrl(url) {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

// XSS 过滤
function sanitizeInput(input) {
  return input
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// 长度限制
function validateLength(input, min, max) {
  return input.length >= min && input.length <= max;
}

服务端验证(必须)

// Express 验证中间件
const { body, validationResult } = require('express-validator');

const registerValidation = [
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Invalid email'),

  body('password')
    .isLength({ min: 8 })
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage('Password must meet requirements'),

  body('username')
    .trim()
    .isLength({ min: 3, max: 20 })
    .matches(/^[a-zA-Z0-9_]+$/)
    .withMessage('Username must be alphanumeric')
];

app.post('/register', registerValidation, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

  // 验证通过,继续处理
});

认证与授权

JWT 安全

// JWT 结构
// Header.Payload.Signature

// 安全考虑
const jwtSecret = process.env.JWT_SECRET;  // 必须使用强密钥

// Token 有效期
const accessToken = jwt.sign(
  { userId: user.id, role: user.role },
  jwtSecret,
  { expiresIn: '15m' }  // 短有效期
);

const refreshToken = jwt.sign(
  { userId: user.id },
  jwtSecret,
  { expiresIn: '7d' }  // 稍长有效期
);

// Token 泄露处理 - 黑名单
const tokenBlacklist = new Set();

app.post('/logout', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (token) {
    tokenBlacklist.add(token);
  }
  res.json({ message: 'Logged out' });
});

// Token 验证时检查黑名单
function verifyToken(token) {
  if (tokenBlacklist.has(token)) {
    throw new Error('Token revoked');
  }
  return jwt.verify(token, jwtSecret);
}

OAuth 2.0 安全

// 1. State 参数防 CSRF
const state = crypto.randomBytes(16).toString('hex');
session.oauthState = state;

// 重定向到授权服务器
// https://auth.example.com/authorize?
//   client_id=xxx&
//   redirect_uri=https://app.example.com/callback&
//   state=${state}&
//   response_type=code&
//   scope=read

// 2. 回调验证 state
app.get('/callback', (req, res) => {
  const { code, state } = req.query;

  if (state !== session.oauthState) {
    return res.status(400).send('State mismatch');
  }

  // 交换 token
  // ...
});

// 3. PKCE (Proof Key for Code Exchange)
// 用于 public client(如 SPA、移动端)
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// 授权请求包含 code_challenge
// token 请求携带 code_verifier

HTTPS 与安全 Headers

安全响应头

# Nginx 配置安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

安全头说明

Header 作用
X-Frame-Options 防止点击劫持
X-Content-Type-Options 防止 MIME 类型嗅探
X-XSS-Protection XSS 过滤器(现代浏览器已内置)
Referrer-Policy 控制 Referer 头
Permissions-Policy 控制浏览器功能权限
Strict-Transport-Security 强制 HTTPS

WebAuthn(无密码认证)

// 注册
async function register() {
  const credential = await navigator.credentials.create({
    publicKey: {
      challenge: new Uint8Array([...randomBytes]),
      rp: {
        name: 'My App',
        id: 'example.com'
      },
      user: {
        id: new Uint8Array([...userIdBytes]),
        name: '[email protected]',
        displayName: 'User Name'
      },
      pubKeyCredParams: [
        { alg: -7, type: 'public-key' },   // ES256
        { alg: -257, type: 'public-key' }  // RS256
      ],
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        userVerification: 'required'
      }
    }
  });

  // 发送 credential 到服务器存储
  return credential;
}

// 认证
async function authenticate() {
  const assertion = await navigator.credentials.get({
    publicKey: {
      challenge: new Uint8Array([...randomBytes]),
      allowCredentials: [{
        id: storedCredentialId,
        type: 'public-key'
      }],
      userVerification: 'required'
    }
  });

  // 验证 assertion
  return assertion;
}

依赖安全

# npm audit
npm audit

# 修复漏洞
npm audit fix

# Snyk
npx snyk test

# GitHub 依赖审查(自动)
# 在 GitHub Settings → Security → Dependency review

依赖安全策略

// package.json
{
  "scripts": {
    "security:audit": "npm audit --audit-level=high",
    "security:check": "npx snyk test"
  }
}

这一章想说的

前端安全是必须重视的领域:

  1. XSS:永远不要信任用户输入,使用转义或白名单
  2. CSRF:使用 Token、SameSite Cookie
  3. CSP:内容安全策略限制脚本执行
  4. CORS:正确配置跨域请求
  5. 输入验证:前后端双重验证
  6. 安全 Headers:配置完善的安全响应头

安全不是事后补救,而是设计时就要考虑。


延展阅读