Web 安全速查表
本文档提供 Web 安全防御的各种配置、代码片段和最佳实践清单。建议开发者在编码和部署时对照检查。
1. SQL 注入防护
参数化查询速查
| 语言/框架 | 安全写法 | 说明 |
|---|---|---|
| Node.js (mysql2) | db.query('SELECT * FROM users WHERE id = ?', [id]) | 使用 ? 占位符 |
| Node.js (pg) | pool.query('SELECT * FROM users WHERE id = $1', [id]) | 使用 $1, $2 占位符 |
| Python (sqlite3) | cursor.execute('SELECT * FROM users WHERE id = ?', (id,)) | 使用 ? 占位符 |
| Python (psycopg2) | cursor.execute('SELECT * FROM users WHERE id = %s', (id,)) | 使用 %s 占位符 |
| Java (JDBC) | PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?") | 使用预编译语句 |
| Java (MyBatis) | SELECT * FROM users WHERE id = #{id} | 使用 #{}(不是 ${}) |
| Go (database/sql) | db.Query("SELECT * FROM users WHERE id = ?", id) | 使用 ? 占位符 |
| PHP (PDO) | $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]) | 使用预编译语句 |
危险模式检查
# 搜索代码中的危险模式
grep -r "SELECT.*+.*" . # 字符串拼接
grep -r '\${' . --include="*.xml" # MyBatis 字符串替换
grep -r "queryRawUnsafe" . # Prisma 不安全查询
grep -r "execute(" . # 原生 SQL 执行
2. XSS 防护
输出编码规则
| 上下文 | 编码方法 | 示例 |
|---|---|---|
| HTML 内容 | HTML 实体编码 | < → < > → > & → & |
| HTML 属性 | 属性编码 + 引号包围 | " → " ' → ' |
| JavaScript | Unicode 编码 | " → \u0022 |
| URL | 百分号编码 | → %20 |
| CSS | 反斜杠十六进制 | " → \000022 |
安全的 DOM 操作
// ✅ 安全:自动编码
element.textContent = userInput;
element.setAttribute('data-value', userInput);
element.className = userInput;
// ❌ 危险:不编码
element.innerHTML = userInput;
document.write(userInput);
框架安全边界
| 框架 | 安全用法 | 危险用法 |
|---|---|---|
| React | <div>{input}</div> | dangerouslySetInnerHTML={{__html: input}} |
| Vue | {{ input }} | v-html="input" |
| Angular | {{ input }} | [innerHTML]="input" |
富文本净化
import DOMPurify from 'dompurify';
// 基本净化
const clean = DOMPurify.sanitize(dirtyHTML);
// 允许特定标签
const clean = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['p', 'b', 'i', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title', 'target']
});
3. CSRF 防护
Cookie 安全配置
Set-Cookie: session=xxx;
HttpOnly; /* 禁止 JS 读取,防 XSS */
Secure; /* 仅 HTTPS 传输 */
SameSite=Lax; /* 防跨站请求 */
Path=/;
Max-Age=3600
SameSite 属性选择
| 值 | 行为 | 适用场景 |
|---|---|---|
Strict | 完全禁止跨站发送 | 敏感操作、支付 |
Lax | 允许顶级导航 GET 携带 | 大多数 Cookie(默认推荐) |
None | 允许跨站(需 Secure) | OAuth 回调、第三方集成 |
CSRF Token 实现
// 服务端:生成 Token
const crypto = require('crypto');
const csrfToken = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = csrfToken;
// 服务端:验证 Token
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('CSRF 验证失败');
}
// 前端:表单中携带
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
// 前端:AJAX Header 携带
headers: { 'X-CSRF-Token': csrfToken }
4. HTTP 安全头配置
完整安全头配置
# 强制 HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# 防止 MIME 嗅探
X-Content-Type-Options: nosniff
# 防止点击劫持
X-Frame-Options: SAMEORIGIN
# XSS 保护(现代浏览器已内置,但保留兼容)
X-XSS-Protection: 1; mode=block
# 内容安全策略
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
# 引用策略
Referrer-Policy: strict-origin-when-cross-origin
# 权限策略
Permissions-Policy: geolocation=(), microphone=(), camera=()
Nginx 配置
server {
# 强制 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 安全头
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# CSP(根据应用调整)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always;
# 移除敏感信息
server_tokens off;
}
Apache 配置
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "default-src 'self'"
</IfModule>
# 移除服务器签名
ServerSignature Off
ServerTokens Prod
Express.js 配置
const helmet = require('helmet');
app.use(helmet()); // 应用所有安全头
// 或单独配置
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:'],
}
}));
5. 密码安全
密码哈希算法选择
| 算法 | 推荐度 | 说明 |
|---|---|---|
| Argon2 | ⭐⭐⭐⭐⭐ | 最推荐,抗 GPU 破解 |
| bcrypt | ⭐⭐⭐⭐ | 广泛支持,成熟稳定 |
| scrypt | ⭐⭐⭐⭐ | 内存困难,抗 ASIC |
| PBKDF2 | ⭐⭐⭐ | 标准算法,兼容性好 |
| SHA256/MD5 | ❌ | 不推荐,易破解 |
密码哈希实现
// Node.js - bcrypt
const bcrypt = require('bcrypt');
const hashedPassword = await bcrypt.hash(password, 12); // cost factor 12
const isValid = await bcrypt.compare(password, hashedPassword);
// Python - bcrypt
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
is_valid = bcrypt.checkpw(password.encode(), hashed)
// Java - BCrypt
import org.mindrot.jbcrypt.BCrypt;
String hashed = BCrypt.hashpw(password, BCrypt.gensalt(12));
boolean isValid = BCrypt.checkpw(password, hashed);
// Go - bcrypt
import "golang.org/x/crypto/bcrypt"
hashed, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
err := bcrypt.CompareHashAndPassword(hashed, []byte(password))
密码强度要求
function validatePassword(password) {
const rules = {
minLength: password.length >= 12,
hasUpper: /[A-Z]/.test(password),
hasLower: /[a-z]/.test(password),
hasNumber: /[0-9]/.test(password),
hasSpecial: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password),
notCommon: !commonPasswords.includes(password.toLowerCase())
};
return Object.values(rules).every(Boolean);
}
6. 加密与数据保护
加密算法选择速查
| 场景 | 推荐算法 | 密钥长度 | 说明 |
|---|---|---|---|
| 密码存储 | Argon2id | - | 首选,抗 GPU/ASIC |
| 密码存储(备选) | bcrypt | - | 成熟稳定 |
| 对称加密 | AES-256-GCM | 256 位 | 认证加密 |
| 移动端加密 | ChaCha20-Poly1305 | 256 位 | 无需硬件加速 |
| 非对称加密 | ECDSA/Ed25519 | 256 位 | 比 RSA 更高效 |
| 密钥交换 | ECDH(Curve25519) | 256 位 | TLS 1.3 默认 |
| 数字签名 | Ed25519 | 256 位 | 高效安全 |
| 哈希 | SHA-256/SHA-3 | - | 通用安全哈希 |
Argon2id 推荐配置
| 内存 (m) | 迭代次数 (t) | 并行度 (p) | 适用场景 |
|---|---|---|---|
| 46 MiB | 1 | 1 | 内存充足 |
| 19 MiB | 2 | 1 | 平衡配置(推荐) |
| 12 MiB | 3 | 1 | 内存受限 |
AES-GCM 快速实现
// Node.js - AES-GCM 加密
const crypto = require('crypto');
function encrypt(key, plaintext) {
const iv = crypto.randomBytes(12); // 12 字节 IV
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}
function decrypt(key, encryptedData) {
const data = Buffer.from(encryptedData, 'base64');
const iv = data.subarray(0, 12);
const authTag = data.subarray(12, 28);
const encrypted = data.subarray(28);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return decrypted.toString('utf8');
}
安全随机数生成
| 语言 | 不安全 | 安全 |
|---|---|---|
| JavaScript | Math.random() | crypto.randomBytes() |
| Python | random.random() | secrets.token_bytes() |
| Java | java.util.Random | java.security.SecureRandom |
| Go | math/rand | crypto/rand |
| PHP | rand() | random_bytes() |
TLS 配置速查
# Nginx - 现代 TLS 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
密钥管理原则
| 原则 | 说明 |
|---|---|
| 不硬编码 | 密钥不能写入代码或配置文件 |
| 分离存储 | 密钥与加密数据分开存放 |
| 最小权限 | 只授予必要的密钥访问权限 |
| 定期轮换 | 按策略定期更换密钥 |
| 使用 KMS | 优先使用云密钥管理服务 |
数据脱敏规则
| 数据类型 | 脱敏规则 | 示例 |
|---|---|---|
| 手机号 | 保留前 3 后 4 | 138****5678 |
| 身份证 | 保留前 3 后 4 | 310***********1234 |
| 银行卡 | 保留前 4 后 4 | 6222 **** **** 7890 |
| 邮箱 | 保留首尾字符 | t***[email protected] |
| 姓名 | 保留首字 | 张** |
8. 身份认证
JWT 安全配置
// 安全的 JWT 配置
const token = jwt.sign(
{ userId: user.id, role: user.role }, // 只放必要信息
process.env.JWT_SECRET, // 密钥从环境变量读取
{
algorithm: 'RS256', // 使用非对称算法
expiresIn: '15m', // 短有效期
issuer: 'your-app',
audience: 'your-app-users'
}
);
// 验证时严格检查
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // 明确指定算法
issuer: 'your-app',
audience: 'your-app-users'
});
会话安全配置
// Express.js 会话配置
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // 防 XSS
secure: true, // 仅 HTTPS
sameSite: 'strict', // 防 CSRF
maxAge: 3600000 // 1 小时
}
}));
登录安全措施
// 防暴力破解
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 最多 5 次尝试
message: '尝试次数过多,请稍后再试'
});
app.post('/login', loginLimiter, (req, res) => {
// 登录逻辑
});
// 账户锁定
const failedAttempts = new Map();
function checkLockout(username) {
const attempts = failedAttempts.get(username) || 0;
if (attempts >= 5) {
const lockoutTime = lockouts.get(username);
if (lockoutTime && Date.now() < lockoutTime) {
return { locked: true, remainingTime: lockoutTime - Date.now() };
}
}
return { locked: false };
}
9. 依赖安全
定期扫描
# Node.js
npm audit
npm audit fix
# 使用 Snyk
npx snyk test
npx snyk monitor
# Python
pip-audit
safety check
# Go
go list -m -u all
trivy fs .
# Java (Maven)
mvn dependency-check:check
依赖固定
// package-lock.json - 始终提交
// 使用 npm ci 而非 npm install
// Pipfile.lock - 始终提交
// pipenv sync
// go.sum - 始终提交
CI/CD 集成
# GitHub Actions
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run npm audit
run: npm audit --audit-level=high
- name: Run Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'HIGH,CRITICAL'
10. 安全日志
应记录的安全事件
| 事件类型 | 示例 |
|---|---|
| 认证事件 | 登录成功/失败、登出、密码重置 |
| 授权事件 | 权限检查失败、越权尝试 |
| 敏感操作 | 数据导出、配置变更、删除操作 |
| 异常行为 | 速率限制触发、异常 IP 访问 |
| 系统事件 | 服务启动/停止、配置加载 |
日志格式
// 安全日志结构
const securityLog = {
timestamp: '2024-01-15T10:30:00Z',
eventType: 'LOGIN_FAILURE',
userId: 'user123',
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
details: {
reason: 'INVALID_PASSWORD',
attempts: 3
},
requestId: 'req-abc-123'
};
日志安全原则
- ❌ 不记录密码、令牌、信用卡号等敏感数据
- ❌ 不记录完整的请求体或响应体
- ✅ 记录用户标识、IP、时间、操作类型
- ✅ 使用结构化日志格式(JSON)
- ✅ 集中存储,限制访问权限
11. 安全检查清单
上线前检查
认证授权
- 密码使用安全哈希存储(bcrypt/Argon2)
- 实施密码强度要求
- 敏感操作需要重新认证
- 会话有合理的超时时间
- 登出后彻底销毁会话
输入验证
- 所有外部输入都经过验证
- 使用白名单验证而非黑名单
- 服务端验证(不依赖前端)
- 文件上传有类型和大小限制
输出编码
- 根据上下文正确编码输出
- 使用框架的安全特性
- 富文本使用 DOMPurify 净化
访问控制
- 实施最小权限原则
- 每个请求都验证权限
- 不暴露内部实现细节
加密
- 强制 HTTPS
- Cookie 设置 Secure 标志
- 敏感数据加密存储
- 密钥安全存储
配置
- 移除默认凭据
- 禁用不必要的功能
- 设置安全 HTTP 头
- 错误信息不泄露敏感数据
依赖
- 依赖已通过安全扫描
- 使用最新稳定版本
- 锁定依赖版本
12. 常用工具
安全扫描
| 工具 | 类型 | 说明 |
|---|---|---|
| OWASP ZAP | DAST | 动态应用安全测试 |
| Burp Suite | DAST | 渗透测试工具 |
| SonarQube | SAST | 静态代码分析 |
| Snyk | SCA | 依赖漏洞扫描 |
| Trivy | 容器安全 | 容器镜像扫描 |
| SQLMap | 专用 | SQL 注入检测 |
在线资源
- OWASP Cheat Sheet Series
- OWASP Testing Guide
- Security Headers - 检查 HTTP 安全头
- SSL Labs - SSL/TLS 配置测试
- Have I Been Pwned - 密码泄露检查