安全评审清单

安全评审关注系统的安全风险——认证授权、数据保护、输入校验、攻击面、合规。安全问题一旦暴露,修复成本极高(数据泄露、用户信任、法律责任)。在设计阶段就把安全考虑进去,比事后补救划算得多。

要点

  • 安全评审关注认证授权、数据保护、输入校验、攻击面、合规
  • 安全问题修复成本极高,设计阶段就要考虑
  • 重点检查:密码存储、权限控制、输入校验、密钥管理、敏感数据加密
  • 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. 评审会上容易漏的点

  1. 越权访问:问「用户 A 能不能访问用户 B 的数据?」
  2. 日志泄露:问「日志里会不会打印密码或 token?」
  3. 错误信息:问「错误响应会不会暴露内部细节?」
  4. 密钥管理:问「密钥存在哪里?泄露了怎么办?」
  5. 合规要求:问「这个功能需要满足哪些法规?」

5. 通过标准

  • 认证授权机制完整
  • 输入校验到位(无注入风险)
  • 敏感数据加密存储和传输
  • 密钥管理规范
  • 依赖无已知高危漏洞
  • 审计日志完整
  • 合规要求满足

6. 不通过的情况

以下情况应该打回重做:

  • 密码明文存储
  • SQL 注入漏洞
  • 密钥硬编码在代码里
  • 没有权限检查
  • 缺少必要的加密
  • 合规要求未满足

7. 维护建议

安全评审清单需要随威胁环境更新:

  • 新型攻击手法出现时,补充对应检查项
  • 出现安全事件后,把根因对应的检查项加进来
  • 合规要求变化时,同步调整相关检查项
  • 每半年复审一次,删除过时的检查项