Web 安全是前端开发不可忽视的重要领域。了解常见的安全威胁和防护措施,是每个开发者的必修课。
| 威胁 | 说明 | 危害等级 |
|---|---|---|
| XSS | 跨站脚本攻击 | 高 |
| CSRF | 跨站请求伪造 | 高 |
| SQL 注入 | 数据库攻击 | 高 |
| 点击劫持 | 界面欺骗 | 中 |
| 中间人攻击 | 数据窃取 | 高 |
| 敏感数据泄露 | 信息泄露 | 高 |
XSS(Cross-Site Scripting)是攻击者向网页注入恶意脚本的攻击方式。
| 类型 | 说明 | 示例 |
|---|---|---|
| 反射型 | URL 参数注入 | ?name=<script>alert(1)</script> |
| 存储型 | 存储在服务器 | 评论、用户信息 |
| DOM 型 | 客户端注入 | innerHTML |
// 危险代码 - 不要这样写
document.getElementById('output').innerHTML = userInput
// 攻击者输入
const maliciousInput = '<img src=x onerror="alert(document.cookie)">'
// 结果:恶意脚本执行
// 1. 使用 textContent 而非 innerHTML
element.textContent = userInput
// 2. HTML 转义
function escapeHtml(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
}
return str.replace(/[&<>"'/]/g, char => escapeMap[char])
}
// 使用
element.innerHTML = escapeHtml(userInput)
// 3. 使用 DOMPurify 库
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(dirtyHtml)
element.innerHTML = clean
// 4. Content Security Policy
// HTTP 响应头
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
// 5. HttpOnly Cookie
// 服务端设置
// Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
// React 自动转义
function SafeComponent({ userInput }) {
return <div>{userInput}</div> // 自动转义
}
// 危险:dangerouslySetInnerHTML
function DangerousComponent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
// 安全使用
import DOMPurify from 'dompurify'
function SafeHtmlComponent({ html }) {
const clean = DOMPurify.sanitize(html)
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
<template>
<!-- 自动转义 -->
<div>{{ userInput }}</div>
<!-- 危险:v-html -->
<div v-html="html"></div>
</template>
<script>
import DOMPurify from 'dompurify'
export default {
data() {
return {
userInput: '<script>alert(1)</script>'
}
},
computed: {
safeHtml() {
return DOMPurify.sanitize(this.html)
}
}
}
</script>
CSRF(Cross-Site Request Forgery)是攻击者诱导用户在已登录网站上执行非预期操作。
<!-- 恶意网站上的代码 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">
<!-- 或隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="stealForm">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.getElementById('stealForm').submit()
</script>
// 1. CSRF Token
// 服务端生成 Token
app.get('/form', (req, res) => {
const csrfToken = generateCSRFToken()
res.cookie('csrfToken', csrfToken)
res.render('form', { csrfToken })
})
// 前端提交时携带
const form = document.getElementById('myForm')
const csrfToken = getCookie('csrfToken')
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
})
// 2. SameSite Cookie
// Set-Cookie: sessionId=abc; SameSite=Strict
// SameSite=Strict 完全禁止跨站发送
// SameSite=Lax 允许安全的跨站请求
// SameSite=None 允许跨站发送(需要 Secure)
// 3. 验证 Referer/Origin
app.post('/api/action', (req, res) => {
const origin = req.get('Origin') || req.get('Referer')
if (!origin || !origin.startsWith('https://mysite.com')) {
return res.status(403).json({ error: 'CSRF 验证失败' })
}
// 处理请求
})
// 4. 双重 Cookie 验证
// 将 Token 同时放在 Cookie 和请求参数中
function submitWithDoubleCookie(data) {
const token = getCookie('csrfToken')
return fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, _csrf: token })
})
}
点击劫持是攻击者将目标网站嵌入隐藏的 iframe 中,诱导用户点击。
<!-- 恶意网站 -->
<style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
.fake-button {
position: absolute;
top: 100px;
left: 100px;
}
</style>
<iframe src="https://bank.com/transfer"></iframe>
<div class="fake-button">点击领取奖品</div>
// 1. X-Frame-Options 响应头
// X-Frame-Options: DENY // 禁止嵌入
// X-Frame-Options: SAMEORIGIN // 只允许同源嵌入
// X-Frame-Options: ALLOW-FROM uri // 允许特定来源
// Express 设置
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY')
next()
})
// 2. Content-Security-Policy
// Content-Security-Policy: frame-ancestors 'self'
// 3. JavaScript 检测
if (window.top !== window.self) {
window.top.location = window.self.location
}
// 或
<style>body { display: none; }</style>
<script>
if (self === top) {
document.documentElement.style.display = 'block'
} else {
top.location = self.location
}
</script>
// Express 中设置安全头
const helmet = require('helmet')
app.use(helmet())
// 或手动设置
app.use((req, res, next) => {
// 防止 MIME 类型嗅探
res.setHeader('X-Content-Type-Options', 'nosniff')
// XSS 保护
res.setHeader('X-XSS-Protection', '1; mode=block')
// 禁止嵌入 iframe
res.setHeader('X-Frame-Options', 'DENY')
// HSTS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
// CSP
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.db-w.cn"
)
// Referrer 策略
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
// 权限策略
res.setHeader('Permissions-Policy',
'geolocation=(), microphone=(), camera=()'
)
next()
})
// 1. 密码加密存储
const bcrypt = require('bcrypt')
// 加密密码
async function hashPassword(password) {
const salt = await bcrypt.genSalt(10)
return bcrypt.hash(password, salt)
}
// 验证密码
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash)
}
// 2. 密码强度验证
function validatePassword(password) {
const rules = {
minLength: password.length >= 8,
hasLower: /[a-z]/.test(password),
hasUpper: /[A-Z]/.test(password),
hasNumber: /\d/.test(password),
hasSpecial: /[!@#$%^&*]/.test(password)
}
return {
valid: Object.values(rules).every(Boolean),
rules
}
}
// 3. 密码输入安全
// 使用 type="password"
<input type="password" name="password" autocomplete="new-password">
// 禁止复制粘贴密码(可选)
<input type="password" oncopy="return false" onpaste="return false">
// 1. 不在前端存储敏感数据
// 错误
localStorage.setItem('token', sensitiveToken)
localStorage.setItem('password', password)
// 正确:使用 HttpOnly Cookie 或内存存储
let sessionToken = null // 内存中,页面刷新后清除
// 2. 敏感信息脱敏
function maskEmail(email) {
const [name, domain] = email.split('@')
const maskedName = name.slice(0, 2) + '***'
return `${maskedName}@${domain}`
}
function maskPhone(phone) {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
function maskIdCard(idCard) {
return idCard.replace(/(\d{4})\d{10}(\d{4})/, '$1**********$2')
}
// 3. 日志安全
// 错误
console.log('用户登录:', { username, password })
// 正确
console.log('用户登录:', { username })
// 生产环境移除 console
if (process.env.NODE_ENV === 'production') {
console.log = () => {}
console.debug = () => {}
}
// 4. 错误信息处理
// 错误:暴露敏感信息
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack
})
})
// 正确:生产环境隐藏详情
app.use((err, req, res, next) => {
res.status(500).json({
error: '服务器错误',
...(process.env.NODE_ENV === 'development' && {
message: err.message,
stack: err.stack
})
})
})
// 1. 前端验证
const validator = {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
phone: (value) => /^1[3-9]\d{9}$/.test(value),
url: (value) => {
try {
new URL(value)
return true
} catch {
return false
}
},
number: (value) => !isNaN(parseFloat(value)) && isFinite(value),
integer: (value) => Number.isInteger(Number(value)),
minLength: (value, min) => value.length >= min,
maxLength: (value, max) => value.length <= max,
range: (value, min, max) => value >= min && value <= max
}
// 2. 后端验证(express-validator)
const { body, validationResult } = require('express-validator')
app.post('/api/register',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('phone').isMobilePhone('zh-CN'),
body('name').trim().escape(),
(req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
// 处理请求
}
)
// 3. SQL 注入防护
// 错误
const query = `SELECT * FROM users WHERE id = ${userId}`
// 正确:使用参数化查询
const query = 'SELECT * FROM users WHERE id = ?'
db.query(query, [userId])
// 或使用 ORM
const user = await User.findById(userId)
了解 Web 安全后,你可以继续学习:
东巴文(db-w.cn)—— 让 Web 更安全