安全评审清单
安全评审关注系统的安全风险——认证授权、数据保护、输入校验、攻击面、合规。安全问题一旦暴露,修复成本极高(数据泄露、用户信任、法律责任)。在设计阶段就把安全考虑进去,比事后补救划算得多。
要点
- 安全评审关注认证授权、数据保护、输入校验、攻击面、合规
- 安全问题修复成本极高,设计阶段就要考虑
- 重点检查:密码存储、权限控制、输入校验、密钥管理、敏感数据加密
- Hono 提供中间件机制实现认证授权,zod 做输入校验
1. 适用时机
- 新功能涉及用户数据、认证、授权、支付
- 对外暴露新接口(API、Webhook)
- 引入新的第三方服务或 SDK
- 处理敏感数据(密码、身份证、银行卡、医疗记录)
- 合规要求(GDPR、等保、PCI DSS、HIPAA)
2. 评审前准备
- 数据分类:系统处理哪些数据?哪些是敏感的?
- 威胁模型:可能的攻击者是谁?攻击面在哪里?
- 合规要求:需要满足哪些法规或标准?
- 已有安全措施:当前有哪些安全机制?
- 历史安全事件:过去出过什么安全问题?
3. 核心检查项
3.1 认证
- 认证机制明确(密码、OTP、OAuth、SAML、WebAuthn)
- 密码存储使用强哈希(bcrypt、argon2、scrypt),不是 MD5/SHA1
- 密码策略合理(长度、复杂度、历史密码检查)
- 登录失败限制(防暴力破解:失败次数、锁定时间、验证码)
- Session/Token 管理(过期时间、刷新机制、单点登录)
- 多因素认证(敏感操作、高价值账户)
Hono 示例:JWT 认证
import { Hono } from 'hono'
import { sign, verify } from 'hono/jwt'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// 登录接口
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()
// 查找用户
const user = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(email)
.first()
if (!user) {
throw new HTTPException(401, { message: '邮箱或密码错误' })
}
// 验证密码(使用 bcrypt)
const valid = await verifyPassword(password, user.passwordHash)
if (!valid) {
// 记录失败次数
await recordLoginFailure(c.env, email)
throw new HTTPException(401, { message: '邮箱或密码错误' })
}
// 生成 JWT
const token = await sign(
{
userId: user.id,
email: user.email,
role: user.role,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 24 小时过期
},
c.env.JWT_SECRET
)
return c.json({ token, user: { id: user.id, email: user.email } })
})
// JWT 认证中间件
app.use('/api/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new HTTPException(401, { message: '未提供认证令牌' })
}
const token = authHeader.slice(7)
try {
const payload = await verify(token, c.env.JWT_SECRET)
c.set('userId', payload.userId)
c.set('userRole', payload.role)
return next()
} catch (error) {
throw new HTTPException(401, { message: '认证令牌无效或已过期' })
}
})
// 受保护的接口
app.get('/api/profile', (c) => {
const userId = c.get('userId')
return c.json({ userId })
})
// 密码哈希(使用 bcrypt)
async function hashPassword(password: string): Promise<string> {
const salt = await crypto.subtle.generateSalt(16)
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveBits']
)
const derivedBits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256'
},
key,
256
)
return btoa(String.fromCharCode(...new Uint8Array(derivedBits)))
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
const derivedHash = await hashPassword(password)
return derivedHash === hash
}3.2 授权
- 权限模型清晰(RBAC、ABAC)
- 每个接口都有权限检查(不要依赖前端隐藏按钮)
- 权限检查在后端执行(前端只影响 UI,不影响安全)
- 最小权限原则(用户只能访问他需要的资源)
- 越权访问防护(水平越权:A 访问 B 的数据;垂直越权:普通用户访问管理员功能)
- 权限变更即时生效(不需要等用户重新登录)
Hono 示例:权限控制
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// 角色权限中间件
function requireRole(...roles: string[]) {
return async (c: any, next: any) => {
const userRole = c.get('userRole')
if (!roles.includes(userRole)) {
throw new HTTPException(403, { message: '权限不足' })
}
return next()
}
}
// 管理员接口
app.delete('/api/users/:id', requireRole('admin'), async (c) => {
const userId = c.req.param('id')
await c.env.DB.prepare('DELETE FROM users WHERE id = ?')
.bind(userId)
.run()
return c.json({ success: true })
})
// 资源所有权检查(防止水平越权)
app.get('/api/orders/:id', async (c) => {
const orderId = c.req.param('id')
const userId = c.get('userId')
const userRole = c.get('userRole')
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first()
if (!order) {
throw new HTTPException(404, { message: '订单不存在' })
}
// 检查权限:只能查看自己的订单,管理员可以查看所有订单
if (order.userId !== userId && userRole !== 'admin') {
throw new HTTPException(403, { message: '无权访问此订单' })
}
return c.json(order)
})
// 细粒度权限控制
interface Permission {
resource: string
action: 'read' | 'write' | 'delete'
}
async function checkPermission(
db: D1Database,
userId: string,
permission: Permission
): Promise<boolean> {
const userPerms = await db
.prepare('SELECT * FROM user_permissions WHERE user_id = ?')
.bind(userId)
.all()
return userPerms.results.some(
p => p.resource === permission.resource && p.action === permission.action
)
}
app.post('/api/reports', async (c) => {
const userId = c.get('userId')
const hasPermission = await checkPermission(c.env.DB, userId, {
resource: 'reports',
action: 'write'
})
if (!hasPermission) {
throw new HTTPException(403, { message: '无权创建报告' })
}
// ... 创建报告
})3.3 输入校验
- 所有用户输入都校验(类型、长度、格式、范围)
- SQL 注入防护(参数化查询、ORM)
- XSS 防护(输出编码、CSP、HttpOnly Cookie)
- CSRF 防护(Token、SameSite Cookie)
- 命令注入防护(不拼接 shell 命令)
- 文件上传校验(类型、大小、内容、存储路径)
- 路径遍历防护(不直接拼接用户输入到文件路径)
Hono 示例:输入校验
import { Hono } from 'hono'
import { z } from 'zod'
import { validator } from 'hono/validator'
const app = new Hono()
// 使用 Zod 校验请求体
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin']).default('user')
})
app.post('/api/users', async (c) => {
const body = await c.req.json()
const result = CreateUserSchema.safeParse(body)
if (!result.success) {
return c.json({
error: 'VALIDATION_ERROR',
details: result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}))
}, 400)
}
const user = await createUser(c.env, result.data)
return c.json(user, 201)
})
// 路径参数校验
app.get('/api/users/:id', validator('param', (value, c) => {
const id = value.id
if (!/^[a-zA-Z0-9-]+$/.test(id)) {
return c.json({ error: 'Invalid user ID' }, 400)
}
return value
}), async (c) => {
const userId = c.req.valid('param').id
const user = await findUser(c.env, userId)
return c.json(user)
})
// 文件上传校验
app.post('/api/upload', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file')
if (!(file instanceof File)) {
return c.json({ error: 'No file provided' }, 400)
}
// 校验文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (!allowedTypes.includes(file.type)) {
return c.json({ error: 'Invalid file type' }, 400)
}
// 校验文件大小(最大 10MB)
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
return c.json({ error: 'File too large' }, 400)
}
// 校验文件内容(防止恶意文件)
const buffer = await file.arrayBuffer()
const magicBytes = new Uint8Array(buffer.slice(0, 4))
// 检查 JPEG 魔数
if (file.type === 'image/jpeg') {
if (magicBytes[0] !== 0xFF || magicBytes[1] !== 0xD8) {
return c.json({ error: 'Invalid JPEG file' }, 400)
}
}
// 安全存储:使用 UUID 重命名,不保留原始文件名
const safeName = `${crypto.randomUUID()}.${file.type.split('/')[1]}`
await c.env.BUCKET.put(safeName, buffer)
return c.json({ url: `/files/${safeName}` })
})3.4 数据保护
- 敏感数据传输加密(HTTPS、TLS 1.2+)
- 敏感数据存储加密(AES-256、密钥管理)
- 密码不以明文形式出现在任何地方(日志、响应、错误信息)
- PII(个人身份信息)处理合规(最小收集、加密存储、用户可删)
- 数据脱敏(开发、测试、日志环境不用真实数据)
- 数据保留策略(多久后删除、如何删除)
Hono 示例:敏感数据加密
import { Hono } from 'hono'
const app = new Hono()
// 加密敏感字段
async function encryptField(value: string, key: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(value)
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
'AES-GCM',
false,
['encrypt']
)
const iv = crypto.getRandomValues(new Uint8Array(12))
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
data
)
// 返回 IV + 加密数据
const combined = new Uint8Array(iv.length + encrypted.byteLength)
combined.set(iv)
combined.set(new Uint8Array(encrypted), iv.length)
return btoa(String.fromCharCode(...combined))
}
async function decryptField(encrypted: string, key: string): Promise<string> {
const combined = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0))
const iv = combined.slice(0, 12)
const data = combined.slice(12)
const encoder = new TextEncoder()
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
'AES-GCM',
false,
['decrypt']
)
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
data
)
return new TextDecoder().decode(decrypted)
}
// 存储加密的用户数据
app.post('/api/users', async (c) => {
const { name, email, phone } = await c.req.json()
// 加密敏感字段
const encryptedEmail = await encryptField(email, c.env.ENCRYPTION_KEY)
const encryptedPhone = await encryptField(phone, c.env.ENCRYPTION_KEY)
await c.env.DB.prepare(
'INSERT INTO users (name, email, phone) VALUES (?, ?, ?)'
)
.bind(name, encryptedEmail, encryptedPhone)
.run()
return c.json({ success: true })
})
// 读取时解密
app.get('/api/users/:id', async (c) => {
const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(c.req.param('id'))
.first()
if (!user) {
return c.json({ error: 'User not found' }, 404)
}
// 解密敏感字段
user.email = await decryptField(user.email, c.env.ENCRYPTION_KEY)
user.phone = await decryptField(user.phone, c.env.ENCRYPTION_KEY)
return c.json(user)
})
// 日志脱敏
function sanitizeLog(data: any): any {
const sanitized = { ...data }
// 脱敏字段
const sensitiveFields = ['password', 'token', 'apiKey', 'creditCard']
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '***'
}
}
return sanitized
}
// 使用
console.log('User login:', sanitizeLog({ email: '[email protected]', password: 'secret' }))
// 输出: { email: '[email protected]', password: '***' }3.5 密钥与凭证管理
- 密钥不硬编码(环境变量、密钥管理服务)
- 密钥不写入代码仓库(.gitignore、pre-commit hook)
- 密钥定期轮换
- 密钥泄露应急预案(立即轮换、审计日志)
- 第三方 API Key 权限最小化
wrangler 密钥管理
# 设置密钥(不会出现在代码中)
wrangler secret put JWT_SECRET
wrangler secret put DATABASE_URL
wrangler secret put API_KEY
# 列出密钥
wrangler secret list
# 删除密钥
wrangler secret delete JWT_SECRET
# 在代码中使用
app.get('/api/data', (c) => {
const apiKey = c.env.API_KEY // 从环境变量读取
// ...
})3.6 依赖与供应链
- 依赖库版本无已知漏洞(CVE 扫描)
- 依赖来源可信(官方 npm/pip/maven,不用来路不明的包)
- 依赖锁定(lock 文件,防止自动升级到恶意版本)
- 依赖更新策略(定期升级、安全补丁及时打)
- CI/CD 管道安全(不受信任的代码不能获得高权限)
3.7 日志与审计
- 关键操作记录审计日志(登录、权限变更、敏感数据访问)
- 日志不包含敏感信息(密码、token、信用卡号)
- 日志防篡改(只写、集中存储)
- 日志保留策略(满足合规要求)
- 异常行为告警(频繁登录失败、异常数据访问)
Hono 示例:审计日志
import { Hono } from 'hono'
const app = new Hono()
// 审计日志中间件
app.use('/api/*', async (c, next) => {
const startTime = Date.now()
await next()
const duration = Date.now() - startTime
const userId = c.get('userId') || 'anonymous'
// 记录审计日志
await c.env.DB.prepare(
'INSERT INTO audit_logs (user_id, action, path, status, duration, ip, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)'
)
.bind(
userId,
c.req.method,
c.req.path,
c.res.status,
duration,
c.req.header('CF-Connecting-IP'),
Date.now()
)
.run()
})
// 敏感操作审计
app.post('/api/users/:id/role', async (c) => {
const userId = c.req.param('id')
const { role } = await c.req.json()
const operatorId = c.get('userId')
// 记录敏感操作
await c.env.DB.prepare(
'INSERT INTO sensitive_operations (operator_id, target_user_id, operation, details, timestamp) VALUES (?, ?, ?, ?, ?)'
)
.bind(operatorId, userId, 'role_change', JSON.stringify({ role }), Date.now())
.run()
// 更新角色
await c.env.DB.prepare('UPDATE users SET role = ? WHERE id = ?')
.bind(role, userId)
.run()
return c.json({ success: true })
})3.8 攻击面
- 暴露的接口最小化(不暴露管理接口、调试接口)
- 错误信息不暴露内部细节(堆栈、SQL、配置)
- HTTP 响应头安全(HSTS、X-Frame-Options、X-Content-Type-Options)
- CORS 配置严格(不
*,明确允许的源) - 速率限制(防滥用、防爬虫)
- DDoS 防护(CDN、流量清洗)
Hono 示例:安全响应头
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// 安全响应头
app.use('*', secureHeaders())
// 自定义安全头
app.use('*', async (c, next) => {
await next()
c.header('X-Frame-Options', 'DENY')
c.header('X-Content-Type-Options', 'nosniff')
c.header('X-XSS-Protection', '1; mode=block')
c.header('Referrer-Policy', 'strict-origin-when-cross-origin')
c.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
// HSTS(强制 HTTPS)
c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
})
// CORS 配置
app.use('/api/*', async (c, next) => {
const allowedOrigins = ['https://example.com', 'https://app.example.com']
const origin = c.req.header('Origin') || ''
if (allowedOrigins.includes(origin)) {
c.header('Access-Control-Allow-Origin', origin)
c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
c.header('Access-Control-Max-Age', '86400')
}
if (c.req.method === 'OPTIONS') {
return c.body(null)
}
await next()
})3.9 合规
- 隐私政策清晰(收集什么数据、用在哪里、保留多久)
- 用户同意机制(明确同意、可撤回)
- 用户数据权利(访问、更正、删除、可携带)
- 数据跨境传输合规
- 行业特定合规(医疗 HIPAA、金融 PCI DSS、欧盟 GDPR)
3.10 安全测试
- 渗透测试(内部或第三方,定期执行)
- 漏洞扫描(自动化,集成到 CI)
- 安全代码审计(关键模块人工审查)
- Bug Bounty 计划(鼓励外部报告漏洞)
- 安全事件响应计划(发现漏洞后的处理流程)
4. 评审会上容易漏的点
- 越权访问:问「用户 A 能不能访问用户 B 的数据?」
- 日志泄露:问「日志里会不会打印密码或 token?」
- 错误信息:问「错误响应会不会暴露内部细节?」
- 密钥管理:问「密钥存在哪里?泄露了怎么办?」
- 合规要求:问「这个功能需要满足哪些法规?」
5. 通过标准
- 认证授权机制完整
- 输入校验到位(无注入风险)
- 敏感数据加密存储和传输
- 密钥管理规范
- 依赖无已知高危漏洞
- 审计日志完整
- 合规要求满足
6. 不通过的情况
以下情况应该打回重做:
- 密码明文存储
- SQL 注入漏洞
- 密钥硬编码在代码里
- 没有权限检查
- 缺少必要的加密
- 合规要求未满足
7. 维护建议
安全评审清单需要随威胁环境更新:
- 新型攻击手法出现时,补充对应检查项
- 出现安全事件后,把根因对应的检查项加进来
- 合规要求变化时,同步调整相关检查项
- 每半年复审一次,删除过时的检查项