{T}

Node.js 安全最佳实践

构建安全的 Node.js 应用需要在开发、部署、运维的每个环节都遵循安全规范。本文档整理了 Node.js 应用的安全编码规范和最佳实践。

安全架构概览

安全防护分层架构

code
┌─────────────────────────────────────────────────────────────────────┐
│                         应用安全层                                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  输入验证    │  │  输出编码    │  │  会话管理    │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  认证授权    │  │  访问控制    │  │  速率限制    │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         传输安全层                                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  HTTPS/TLS   │  │  安全头部    │  │  CORS 策略   │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         数据安全层                                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  数据加密    │  │  密钥管理    │  │  数据脱敏    │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  SQL 注入防护│  │  NoSQL 注入  │  │  备份恢复    │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         基础设施安全层                               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  容器安全    │  │  网络隔离    │  │  依赖安全    │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │  日志审计    │  │  监控告警    │  │  漏洞扫描    │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────────────────────────┘

安全开发流程

code
┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
│  设计阶段  │───▶│  编码阶段  │───▶│  测试阶段  │───▶│  部署阶段  │
└────────────┘    └────────────┘    └────────────┘    └────────────┘
      │                 │                 │                 │
      ▼                 ▼                 ▼                 ▼
┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
│ 威胁建模   │    │ 安全编码   │    │ 安全测试   │    │ 安全配置   │
│ 架构评审   │    │ 代码审查   │    │ 渗透测试   │    │ 漏洞扫描   │
│ 安全需求   │    │ 静态分析   │    │ 依赖审计   │    │ 监控告警   │
└────────────┘    └────────────┘    └────────────┘    └────────────┘

输入验证与输出编码

输入验证原则

永远不要信任任何来自用户的输入,包括:

  • URL 参数
  • 表单数据
  • HTTP 头
  • Cookie
  • 文件上传
  • API 请求体
  • WebSocket 消息

输入验证最佳实践

javascript
const Joi = require('joi')
const { body, param, query, validationResult } = require('express-validator')

// ==================== Joi 验证模式 ====================

const schemas = {
  // 用户注册验证
  register: Joi.object({
    username: Joi.string()
      .alphanum()
      .min(3)
      .max(30)
      .required()
      .messages({
        'string.alphanum': '用户名只能包含字母和数字',
        'string.min': '用户名至少需要 {#limit} 个字符',
        'string.max': '用户名不能超过 {#limit} 个字符'
      }),
    
    email: Joi.string()
      .email()
      .required()
      .messages({
        'string.email': '请输入有效的邮箱地址'
      }),
    
    password: Joi.string()
      .min(8)
      .max(128)
      .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
      .required()
      .messages({
        'string.pattern.base': '密码必须包含大小写字母、数字和特殊字符',
        'string.min': '密码至少需要 {#limit} 个字符'
      }),
    
    age: Joi.number()
      .integer()
      .min(18)
      .max(120)
      .optional(),
    
    phone: Joi.string()
      .pattern(/^1[3-9]\d{9}$/)
      .optional()
      .messages({
        'string.pattern.base': '请输入有效的手机号码'
      })
  }),
  
  // 查询参数验证
  query: Joi.object({
    page: Joi.number().integer().min(1).default(1),
    limit: Joi.number().integer().min(1).max(100).default(10),
    sort: Joi.string().valid('asc', 'desc').default('desc'),
    search: Joi.string().max(100).optional()
  }),
  
  // MongoDB ObjectId 验证
  objectId: Joi.string()
    .pattern(/^[0-9a-fA-F]{24}$/)
    .message('无效的 ID 格式')
}

// ==================== 验证中间件 ====================

function validate(schema, source = 'body') {
  return (req, res, next) => {
    const data = source === 'query' ? req.query : 
                 source === 'params' ? req.params : req.body
    
    const { error, value } = schema.validate(data, {
      abortEarly: false,    // 返回所有错误
      stripUnknown: true,   // 删除未知字段
      convert: true         // 自动类型转换
    })
    
    if (error) {
      return res.status(400).json({
        error: '输入验证失败',
        code: 'VALIDATION_ERROR',
        details: error.details.map(d => ({
          field: d.path.join('.'),
          message: d.message,
          type: d.type
        }))
      })
    }
    
    // 使用验证后的数据替换原始数据
    req[source] = value
    next()
  }
}

// ==================== 使用示例 ====================

// 用户注册
app.post('/register', 
  validate(schemas.register, 'body'),
  async (req, res) => {
    // req.body 已经过验证和清理
    const user = await createUser(req.body)
    res.json({ success: true, user })
  }
)

// 分页查询
app.get('/users',
  validate(schemas.query, 'query'),
  async (req, res) => {
    const { page, limit, sort, search } = req.query
    const users = await getUsers({ page, limit, sort, search })
    res.json(users)
  }
)

// 路径参数验证
app.get('/users/:id',
  validate(Joi.object({ id: schemas.objectId }), 'params'),
  async (req, res) => {
    const user = await User.findById(req.params.id)
    res.json(user)
  }
)

express-validator 示例

javascript
const { body, param, query, validationResult } = require('express-validator')

// 验证结果处理
const validate = (req, res, next) => {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: '输入验证失败',
      details: errors.array().map(err => ({
        field: err.path,
        message: err.msg,
        value: err.value
      }))
    })
  }
  next()
}

// 用户注册验证规则
const registerRules = [
  body('username')
    .trim()
    .isLength({ min: 3, max: 30 })
    .withMessage('用户名长度必须在 3-30 个字符之间')
    .isAlphanumeric()
    .withMessage('用户名只能包含字母和数字'),
  
  body('email')
    .trim()
    .normalizeEmail()
    .isEmail()
    .withMessage('请输入有效的邮箱地址'),
  
  body('password')
    .isLength({ min: 8, max: 128 })
    .withMessage('密码长度必须在 8-128 个字符之间')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage('密码必须包含大小写字母和数字'),
  
  body('confirmPassword')
    .custom((value, { req }) => {
      if (value !== req.body.password) {
        throw new Error('两次输入的密码不一致')
      }
      return true
    }),
  
  validate
]

app.post('/register', registerRules, registerHandler)

输出编码

防止 XSS 攻击,对输出进行编码:

javascript
const escapeHtml = require('escape-html')
const validator = require('validator')

// ==================== HTML 编码 ====================

app.get('/search', (req, res) => {
  const keyword = req.query.q
  const safeKeyword = escapeHtml(keyword)
  res.send(`<p>搜索结果: ${safeKeyword}</p>`)
})

// ==================== URL 编码 ====================

app.get('/redirect', (req, res) => {
  const url = req.query.url
  
  // 验证 URL 格式
  if (!validator.isURL(url, { 
    require_protocol: true,
    protocols: ['http', 'https']
  })) {
    return res.status(400).json({ error: '无效的 URL' })
  }
  
  res.redirect(encodeURIComponent(url))
})

// ==================== JavaScript 编码 ====================

function escapeJs(str) {
  return str
    .replace(/\\/g, '\\\\')
    .replace(/"/g, '\\"')
    .replace(/'/g, "\\'")
    .replace(/</g, '\\x3c')
    .replace(/>/g, '\\x3e')
    .replace(/&/g, '\\x26')
    .replace(/=/g, '\\x3d')
}

// ==================== JSON 安全响应 ====================

app.get('/api/users', (req, res) => {
  const users = getUsers()
  
  // Express 自动进行 JSON 编码,安全
  res.json(users)
  
  // 如果手动拼接 JSON,需要转义
  // const safe = JSON.stringify(users)
  // res.send(safe)
})

// ==================== 模板引擎安全 ====================

// EJS 模板
// <%= userInput %>  // 自动转义(安全)
// <%- userInput %>  // 不转义(危险!)

// 安全使用不转义输出
app.get('/profile', (req, res) => {
  const user = getUser(req.session.userId)
  res.render('profile', {
    user,
    // 预先转义的内容可以安全使用 <%- %>
    safeContent: escapeHtml(user.bio)
  })
})

// ==================== 富文本内容处理 ====================

const sanitizeHtml = require('sanitize-html')

function sanitizeRichText(html) {
  return sanitizeHtml(html, {
    allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
    allowedAttributes: {
      'a': ['href', 'title']
    },
    allowedSchemes: ['http', 'https', 'mailto']
  })
}

app.post('/comment', (req, res) => {
  const content = sanitizeRichText(req.body.content)
  createComment({ content, userId: req.session.userId })
  res.json({ success: true })
})

认证授权最佳实践

密码安全

javascript
const bcrypt = require('bcrypt')
const crypto = require('crypto')

// ==================== 密码哈希 ====================

// 推荐配置
const SALT_ROUNDS = 12 // 10-12 是推荐值,越大越安全但越慢

async function hashPassword(password) {
  // 验证密码强度
  if (!isStrongPassword(password)) {
    throw new Error('密码强度不足')
  }
  
  return bcrypt.hash(password, SALT_ROUNDS)
}

async function verifyPassword(password, hash) {
  // 使用时间恒定比较
  return bcrypt.compare(password, hash)
}

// 密码强度验证
function isStrongPassword(password) {
  const minLength = 8
  const hasLower = /[a-z]/.test(password)
  const hasUpper = /[A-Z]/.test(password)
  const hasNumber = /\d/.test(password)
  const hasSpecial = /[@$!%*?&]/.test(password)
  
  return password.length >= minLength && 
         hasLower && hasUpper && hasNumber && hasSpecial
}

// ==================== 用户注册 ====================

app.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body
    
    // 检查用户是否存在
    const existing = await User.findOne({ $or: [{ email }, { username }] })
    if (existing) {
      return res.status(409).json({ error: '用户已存在' })
    }
    
    // 哈希密码
    const hashedPassword = await hashPassword(password)
    
    // 创建用户
    const user = await User.create({
      username,
      email,
      password: hashedPassword,
      emailVerified: false
    })
    
    // 发送验证邮件
    await sendVerificationEmail(user)
    
    res.status(201).json({ 
      success: true, 
      message: '注册成功,请验证邮箱' 
    })
  } catch (error) {
    logger.error('注册失败', error)
    res.status(500).json({ error: '注册失败' })
  }
})

// ==================== 登录流程 ====================

app.post('/login', loginLimiter, async (req, res) => {
  const { email, password } = req.body
  
  const user = await User.findOne({ email })
  
  // 即使用户不存在也执行哈希比较(防止时序攻击)
  const hashedPassword = user?.password || '$2b$12$dummyhashforfakeuser'
  const isValid = await verifyPassword(password, hashedPassword)
  
  if (!user || !isValid) {
    // 记录失败尝试
    await logFailedLogin(email, req.ip)
    return res.status(401).json({ error: '邮箱或密码错误' })
  }
  
  // 检查账户状态
  if (user.lockedUntil && user.lockedUntil > new Date()) {
    return res.status(403).json({ 
      error: '账户已锁定',
      lockedUntil: user.lockedUntil 
    })
  }
  
  // 登录成功,重置失败计数
  await User.updateOne(
    { _id: user._id },
    { 
      $set: { 
        lastLogin: new Date(),
        loginIp: req.ip
      },
      $unset: { lockedUntil: 1, failedAttempts: 1 }
    }
  )
  
  // 生成令牌
  const token = generateAuthToken(user)
  
  // 记录登录
  logger.info('用户登录', { userId: user._id, ip: req.ip })
  
  res.json({ 
    success: true, 
    token,
    user: { id: user._id, username: user.username, email: user.email }
  })
})

JWT 安全配置

javascript
const jwt = require('jsonwebtoken')

// ==================== JWT 配置 ====================

const JWT_CONFIG = {
  accessToken: {
    secret: process.env.JWT_ACCESS_SECRET,
    expiresIn: '15m'  // 短期令牌
  },
  refreshToken: {
    secret: process.env.JWT_REFRESH_SECRET,
    expiresIn: '7d'   // 长期刷新令牌
  }
}

// ==================== 令牌生成 ====================

function generateTokens(user) {
  const payload = {
    userId: user._id,
    role: user.role,
    // 不要存储敏感信息
  }
  
  const accessToken = jwt.sign(payload, JWT_CONFIG.accessToken.secret, {
    expiresIn: JWT_CONFIG.accessToken.expiresIn,
    issuer: 'your-app',
    audience: 'your-app-users'
  })
  
  const refreshToken = jwt.sign(
    { userId: user._id, tokenId: crypto.randomUUID() },
    JWT_CONFIG.refreshToken.secret,
    { expiresIn: JWT_CONFIG.refreshToken.expiresIn }
  )
  
  return { accessToken, refreshToken }
}

// ==================== 令牌验证中间件 ====================

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization
  
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供认证令牌' })
  }
  
  const token = authHeader.split(' ')[1]
  
  try {
    const decoded = jwt.verify(token, JWT_CONFIG.accessToken.secret, {
      issuer: 'your-app',
      audience: 'your-app-users'
    })
    
    req.user = decoded
    next()
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: '令牌已过期',
        code: 'TOKEN_EXPIRED'
      })
    }
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({ 
        error: '无效的令牌',
        code: 'TOKEN_INVALID'
      })
    }
    return res.status(401).json({ error: '认证失败' })
  }
}

// ==================== 权限检查 ====================

function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: '未认证' })
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: '权限不足' })
    }
    
    next()
  }
}

// 使用示例
app.delete('/users/:id', 
  authMiddleware, 
  requireRole('admin'), 
  deleteUser
)

权限控制

javascript
// ==================== RBAC 权限系统 ====================

const permissions = {
  admin: ['read', 'write', 'delete', 'manage_users'],
  editor: ['read', 'write'],
  viewer: ['read']
}

function hasPermission(user, action) {
  const userPermissions = permissions[user.role] || []
  return userPermissions.includes(action)
}

// 权限中间件
function requirePermission(action) {
  return (req, res, next) => {
    if (!hasPermission(req.user, action)) {
      return res.status(403).json({ 
        error: '权限不足',
        required: action 
      })
    }
    next()
  }
}

// ==================== 资源级权限 ====================

// 检查用户是否有权访问特定资源
async function canAccessResource(userId, resourceId, action) {
  const resource = await Resource.findById(resourceId)
  
  // 资源所有者有完全访问权
  if (resource.ownerId.toString() === userId) {
    return true
  }
  
  // 检查共享权限
  const permission = resource.sharedWith.find(
    share => share.userId.toString() === userId
  )
  
  return permission?.actions.includes(action)
}

// 资源权限中间件
async function checkResourceAccess(req, res, next) {
  const { resourceId } = req.params
  const action = req.method === 'GET' ? 'read' : 
                 req.method === 'DELETE' ? 'delete' : 'write'
  
  const hasAccess = await canAccessResource(
    req.user.userId, 
    resourceId, 
    action
  )
  
  if (!hasAccess) {
    return res.status(403).json({ error: '无权访问此资源' })
  }
  
  next()
}

安全 HTTP 头配置

使用 Helmet 中间件

javascript
const helmet = require('helmet')

// ==================== 基本配置 ====================

app.use(helmet())

// ==================== 详细配置 ====================

app.use(helmet({
  // 内容安全策略
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        "'self'",
        "'unsafe-inline'", // 尽量避免
        "https://cdn.jsdelivr.net"
      ],
      styleSrc: [
        "'self'",
        "'unsafe-inline'",
        "https://fonts.googleapis.com"
      ],
      fontSrc: [
        "'self'",
        "https://fonts.gstatic.com"
      ],
      imgSrc: [
        "'self'",
        "data:",
        "https:",
        "blob:"
      ],
      connectSrc: [
        "'self'",
        "https://api.example.com"
      ],
      frameSrc: ["'none'"],
      objectSrc: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
      frameAncestors: ["'none'"],
      upgradeInsecureRequests: []
    },
    reportOnly: false // 设为 true 可先观察不阻止
  },
  
  // XSS 过滤器(现代浏览器已弃用,但有兼容性)
  xssFilter: true,
  
  // 禁止点击劫持
  frameguard: {
    action: 'deny' // 或 'sameorigin'
  },
  
  // 隐藏 X-Powered-By
  hidePoweredBy: true,
  
  // 防止 MIME 类型嗅探
  noSniff: true,
  
  // HSTS(HTTP 严格传输安全)
  hsts: {
    maxAge: 31536000,      // 1 年
    includeSubDomains: true,
    preload: true
  },
  
  // 禁用 DNS 预解析
  dnsPrefetchControl: {
    allow: false
  },
  
  // 引用策略
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin'
  },
  
  // 权限策略
  permittedCrossDomainPolicies: {
    permittedPolicies: 'none'
  },
  
  // IE 特定
  ieNoOpen: true,
  
  // Origin 隔离
  originAgentCluster: true
}))

// ==================== CSP 报告端点 ====================

app.post('/csp-report', 
  express.json({ type: 'application/csp-report' }),
  (req, res) => {
    logger.warn('CSP 违规', req.body)
    res.status(204).end()
  }
)

HTTP 安全头详解

头部名称作用推荐值
Content-Security-Policy控制资源加载来源根据业务配置白名单
X-Frame-Options防止点击劫持DENY 或 SAMEORIGIN
X-Content-Type-Options防止 MIME 嗅探nosniff
X-XSS-ProtectionXSS 过滤器1; mode=block
Strict-Transport-Security强制 HTTPSmax-age=31536000; includeSubDomains
Referrer-Policy控制引用信息strict-origin-when-cross-origin
Permissions-Policy浏览器功能限制根据需要禁用
X-Permitted-Cross-Domain-Policies跨域策略none
javascript
// ==================== 手动设置安全头 ====================

app.use((req, res, next) => {
  // 移除敏感头
  res.removeHeader('X-Powered-By')
  res.removeHeader('Server')
  
  // 设置安全头
  res.setHeader('X-Content-Type-Options', 'nosniff')
  res.setHeader('X-Frame-Options', 'DENY')
  res.setHeader('X-XSS-Protection', '1; mode=block')
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
  
  // 权限策略
  res.setHeader('Permissions-Policy', [
    'geolocation=()',
    'microphone=()',
    'camera=()',
    'payment=()',
    'usb=()'
  ].join(', '))
  
  // Cache-Control(敏感页面)
  if (req.path.startsWith('/api/') || req.path.startsWith('/account/')) {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
    res.setHeader('Pragma', 'no-cache')
    res.setHeader('Expires', '0')
  }
  
  next()
})

数据安全与加密

敏感数据加密

javascript
const crypto = require('crypto')

// ==================== 对称加密(AES-256-GCM)====================

const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex') // 32字节
const ALGORITHM = 'aes-256-gcm'

function encrypt(text) {
  const iv = crypto.randomBytes(16) // 初始化向量
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv)
  
  let encrypted = cipher.update(text, 'utf8', 'hex')
  encrypted += cipher.final('hex')
  
  const authTag = cipher.getAuthTag() // 认证标签
  
  return {
    iv: iv.toString('hex'),
    encrypted,
    authTag: authTag.toString('hex')
  }
}

function decrypt(encrypted, iv, authTag) {
  const decipher = crypto.createDecipheriv(
    ALGORITHM, 
    ENCRYPTION_KEY, 
    Buffer.from(iv, 'hex')
  )
  
  decipher.setAuthTag(Buffer.from(authTag, 'hex'))
  
  let decrypted = decipher.update(encrypted, 'hex', 'utf8')
  decrypted += decipher.final('utf8')
  
  return decrypted
}

// 使用示例
const sensitive = '用户敏感数据'
const encrypted = encrypt(sensitive)
// 存储到数据库:{ iv, encrypted, authTag }
const decrypted = decrypt(encrypted.encrypted, encrypted.iv, encrypted.authTag)

// ==================== 密码学安全随机数 ====================

function generateSecureToken(bytes = 32) {
  return crypto.randomBytes(bytes).toString('hex')
}

function generateUUID() {
  return crypto.randomUUID()
}

// ==================== 数据脱敏 ====================

function maskEmail(email) {
  const [localPart, domain] = email.split('@')
  const masked = localPart.slice(0, 2) + '***'
  return `${masked}@${domain}`
}

function maskPhone(phone) {
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

function maskIdCard(idCard) {
  return idCard.replace(/(.{6}).*(.{4})/, '$1********$2')
}

function maskBankCard(cardNumber) {
  return '**** **** **** ' + cardNumber.slice(-4)
}

// API 响应脱敏
function sanitizeUserData(user) {
  return {
    id: user._id,
    username: user.username,
    email: maskEmail(user.email),
    phone: user.phone ? maskPhone(user.phone) : undefined,
    // 完全排除敏感字段
    // password, token, apiKey 等不返回
  }
}

密钥管理最佳实践

javascript
// ==================== 环境变量管理 ====================

// 使用 convict 进行配置验证
const convict = require('convict')

const config = convict({
  env: {
    doc: '应用环境',
    format: ['production', 'development', 'test'],
    default: 'development',
    env: 'NODE_ENV'
  },
  
  database: {
    url: {
      doc: '数据库连接 URL',
      format: String,
      default: 'mongodb://localhost:27017/app',
      env: 'DATABASE_URL',
      sensitive: true
    }
  },
  
  jwt: {
    accessSecret: {
      doc: 'JWT 访问令牌密钥',
      format: String,
      default: '',
      env: 'JWT_ACCESS_SECRET',
      sensitive: true
    },
    refreshSecret: {
      doc: 'JWT 刷新令牌密钥',
      format: String,
      default: '',
      env: 'JWT_REFRESH_SECRET',
      sensitive: true
    }
  },
  
  encryption: {
    key: {
      doc: '数据加密密钥(32字节hex)',
      format: String,
      default: '',
      env: 'ENCRYPTION_KEY',
      sensitive: true
    }
  }
})

// 验证配置
config.validate({ allowed: 'strict' })

// ==================== 密钥轮换 ====================

// 支持多密钥验证,新令牌使用新密钥
const JWT_SECRETS = [
  process.env.JWT_SECRET_NEW,    // 新密钥(用于签名)
  process.env.JWT_SECRET_OLD     // 旧密钥(用于验证过渡期)
].filter(Boolean)

function verifyTokenWithRotation(token) {
  for (const secret of JWT_SECRETS) {
    try {
      return jwt.verify(token, secret)
    } catch (e) {
      continue
    }
  }
  throw new Error('Invalid token')
}

// ==================== 密钥生成 ====================

// 生成安全的密钥
function generateEncryptionKey() {
  return crypto.randomBytes(32).toString('hex')
}

function generateJwtSecret() {
  return crypto.randomBytes(64).toString('hex')
}

// 首次部署时生成密钥
// node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

依赖安全与漏洞防护

依赖安全检查

bash
# 安全最佳实践

# 检查漏洞
npm audit

# 自动修复
npm audit fix

# 强制修复(可能有破坏性变更)
npm audit fix --force

# 指定严重级别
npm audit --audit-level=high

# JSON 格式输出
npm audit --json

# ==================== Snyk 深度扫描 ====================

# 安装 Snyk
npm install -g snyk

# 认证
snyk auth

# 扫描
snyk test

# 监控项目
snyk monitor

# 自动修复
snyk wizard

# ==================== 其他工具 ====================

# 检查过期依赖
npx npm-check-updates

# 检查未使用的依赖
npx depcheck

# 检查依赖许可证
npx license-checker --summary

CI/CD 安全检查配置

yaml
# .github/workflows/security.yml
name: Security Audit

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * 0' # 每周检查

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run npm audit
        run: npm audit --audit-level=high
        continue-on-error: true
        
      - name: Run Snyk
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: test
          
      - name: Run Snyk Code (SAST)
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: code test
          
      - name: Check for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}

package.json 安全配置

json
{
  "name": "secure-app",
  "version": "1.0.0",
  "scripts": {
    "audit": "npm audit --audit-level=moderate",
    "audit:fix": "npm audit fix",
    "snyk": "snyk test",
    "preinstall": "npx npm-force-resolutions",
    "postinstall": "npm run audit"
  },
  "dependencies": {
    "express": "4.18.2",
    "helmet": "7.1.0",
    "bcrypt": "5.1.1"
  },
  "devDependencies": {
    "snyk": "1.1260.0"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "resolutions": {
    "lodash": ">=4.17.21",
    "minimist": ">=1.2.6"
  }
}

.npmrc 安全配置

ini
# .npmrc

# 禁止运行脚本(防止恶意脚本)
ignore-scripts=true

# 使用官方 registry
registry=https://registry.npmjs.org/

# 使用 package-lock
package-lock=true

# 使用严格 SSL
strict-ssl=true

# 验证签名
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

# 缓存配置
cache=/tmp/npm-cache

速率限制与防护

基础速率限制

javascript
const rateLimit = require('express-rate-limit')

// ==================== 全局限制 ====================

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100,                 // 每个 IP 最多 100 次请求
  message: {
    error: '请求过于频繁,请稍后重试',
    code: 'RATE_LIMIT_EXCEEDED'
  },
  standardHeaders: true,    // 返回 RateLimit-* 头
  legacyHeaders: false,
  
  // 跳过特定请求
  skip: (req) => {
    const whitelist = ['127.0.0.1', '::1']
    return whitelist.includes(req.ip)
  }
})

app.use(globalLimiter)

// ==================== 严格限制(登录等)====================

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 5,                   // 最多 5 次尝试
  skipSuccessfulRequests: true,
  message: {
    error: '登录尝试次数过多,请 15 分钟后重试',
    code: 'LOGIN_RATE_LIMIT'
  }
})

app.post('/login', loginLimiter, loginHandler)
app.post('/forgot-password', loginLimiter, forgotPasswordHandler)

// ==================== API 分级限制 ====================

const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 分钟
  max: (req) => {
    // 根据用户等级设置不同限制
    if (req.user?.plan === 'enterprise') return 1000
    if (req.user?.plan === 'pro') return 100
    return 20 // 免费用户
  },
  keyGenerator: (req) => {
    // 已登录用户使用 userId
    return req.user?.userId || req.ip
  }
})

app.use('/api/', apiLimiter)

// ==================== 分布式限制(Redis)====================

const RedisStore = require('rate-limit-redis')
const redis = require('redis')

const redisClient = redis.createClient({
  url: process.env.REDIS_URL
})

const distributedLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args)
  }),
  windowMs: 60 * 1000,
  max: 100
})

DDoS 防护

javascript
// ==================== 连接限制 ====================

const express = require('express')
const http = require('http')

const app = express()
const server = http.createServer(app)

// 最大连接数
server.maxConnections = 1000

// 连接超时
server.timeout = 120000 // 2 分钟
server.keepAliveTimeout = 65000

// ==================== 请求体大小限制 ====================

app.use(express.json({ limit: '1mb' }))
app.use(express.urlencoded({ extended: true, limit: '1mb' }))

// ==================== Slowloris 攻击防护 ====================

const slowDown = require('express-slow-down')

const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 100,  // 100 次请求后开始延迟
  delayMs: 500      // 每次请求增加 500ms 延迟
})

app.use(speedLimiter)

// ==================== 超时处理 ====================

const timeout = require('connect-timeout')

app.use(timeout('30s'))

app.use((req, res, next) => {
  if (!req.timedout) next()
})

// ==================== 健康检查(不限制)====================

app.get('/health', (req, res) => {
  res.json({ status: 'ok' })
})

// 从速率限制中排除
app.set('trust proxy', 1)

错误处理与日志安全

安全的错误处理

javascript
// ==================== 自定义错误类 ====================

class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message)
    this.statusCode = statusCode
    this.code = code
    this.isOperational = true
    
    Error.captureStackTrace(this, this.constructor)
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400, 'VALIDATION_ERROR')
  }
}

class AuthenticationError extends AppError {
  constructor(message = '认证失败') {
    super(message, 401, 'AUTHENTICATION_ERROR')
  }
}

class AuthorizationError extends AppError {
  constructor(message = '权限不足') {
    super(message, 403, 'AUTHORIZATION_ERROR')
  }
}

class NotFoundError extends AppError {
  constructor(message = '资源不存在') {
    super(message, 404, 'NOT_FOUND')
  }
}

// ==================== 全局错误处理 ====================

app.use((err, req, res, next) => {
  // 记录错误(服务器端)
  logger.error('请求错误', {
    message: err.message,
    stack: err.stack,
    url: req.originalUrl,
    method: req.method,
    ip: req.ip,
    userId: req.user?.userId,
    body: sanitizeLog(req.body),
    query: req.query
  })
  
  // 判断是否为已知错误
  const isOperational = err.isOperational || false
  
  // 生产环境返回简化信息
  if (process.env.NODE_ENV === 'production') {
    if (isOperational) {
      // 已知错误,返回具体信息
      return res.status(err.statusCode || 500).json({
        error: err.message,
        code: err.code
      })
    }
    
    // 未知错误,返回通用信息
    return res.status(500).json({
      error: '服务器内部错误',
      code: 'INTERNAL_ERROR'
    })
  }
  
  // 开发环境返回详细错误
  res.status(err.statusCode || 500).json({
    error: err.message,
    code: err.code,
    stack: err.stack,
    details: err.details || null
  })
})

// ==================== 异步错误捕获 ====================

// 包装异步路由处理器
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

// 使用示例
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id)
  if (!user) {
    throw new NotFoundError('用户不存在')
  }
  res.json(user)
}))

// ==================== 未捕获异常处理 ====================

process.on('uncaughtException', (error) => {
  logger.error('未捕获异常', error)
  // 优雅关闭
  process.exit(1)
})

process.on('unhandledRejection', (reason, promise) => {
  logger.error('未处理的 Promise 拒绝', { reason, promise })
  process.exit(1)
})

日志安全

javascript
const winston = require('winston')

// ==================== 安全日志配置 ====================

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { 
    service: 'nodejs-app',
    env: process.env.NODE_ENV 
  },
  transports: [
    // 错误日志
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error',
      maxsize: 5242880, // 5MB
      maxFiles: 5
    }),
    // 安全事件日志
    new winston.transports.File({ 
      filename: 'logs/security.log', 
      level: 'warn'
    }),
    // 综合日志
    new winston.transports.File({ 
      filename: 'logs/combined.log',
      maxsize: 10485760, // 10MB
      maxFiles: 10
    })
  ]
})

// 开发环境控制台输出
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    )
  }))
}

// ==================== 日志脱敏 ====================

const SENSITIVE_FIELDS = [
  'password', 'pass', 'pwd',
  'token', 'accessToken', 'refreshToken',
  'secret', 'apiKey', 'api_key',
  'creditCard', 'cardNumber',
  'ssn', 'idCard'
]

function sanitizeLog(data) {
  if (typeof data !== 'object' || data === null) {
    return data
  }
  
  const sanitized = Array.isArray(data) ? [] : {}
  
  for (const [key, value] of Object.entries(data)) {
    const lowerKey = key.toLowerCase()
    
    if (SENSITIVE_FIELDS.some(field => lowerKey.includes(field))) {
      sanitized[key] = '[REDACTED]'
    } else if (typeof value === 'object' && value !== null) {
      sanitized[key] = sanitizeLog(value)
    } else {
      sanitized[key] = value
    }
  }
  
  return sanitized
}

// ==================== 安全事件日志 ====================

const securityLogger = {
  login: (userId, ip, success) => {
    logger.warn('LOGIN_ATTEMPT', {
      userId,
      ip,
      success,
      timestamp: new Date().toISOString()
    })
  },
  
  failedAuth: (ip, reason) => {
    logger.warn('AUTH_FAILURE', {
      ip,
      reason,
      timestamp: new Date().toISOString()
    })
  },
  
  suspiciousRequest: (req, reason) => {
    logger.warn('SUSPICIOUS_REQUEST', {
      ip: req.ip,
      method: req.method,
      url: req.originalUrl,
      headers: sanitizeLog(req.headers),
      reason,
      timestamp: new Date().toISOString()
    })
  },
  
  dataAccess: (userId, resource, action) => {
    logger.info('DATA_ACCESS', {
      userId,
      resource,
      action,
      timestamp: new Date().toISOString()
    })
  },
  
  permissionDenied: (userId, resource, action) => {
    logger.warn('PERMISSION_DENIED', {
      userId,
      resource,
      action,
      timestamp: new Date().toISOString()
    })
  }
}

文件上传安全

安全文件上传配置

javascript
const multer = require('multer')
const path = require('path')
const fs = require('fs').promises
const crypto = require('crypto')

// ==================== 配置常量 ====================

const UPLOAD_CONFIG = {
  // 允许的文件类型
  allowedMimes: [
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/webp',
    'application/pdf',
    'text/plain'
  ],
  
  // 允许的扩展名
  allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf', '.txt'],
  
  // 文件大小限制
  maxSize: 10 * 1024 * 1024, // 10MB
  
  // 上传目录
  uploadDir: path.resolve('./uploads')
}

// ==================== 存储配置 ====================

const storage = multer.diskStorage({
  destination: async (req, file, cb) => {
    // 按日期分目录
    const dateDir = new Date().toISOString().split('T')[0]
    const uploadPath = path.join(UPLOAD_CONFIG.uploadDir, dateDir)
    
    try {
      await fs.mkdir(uploadPath, { recursive: true })
      cb(null, uploadPath)
    } catch (error) {
      cb(error)
    }
  },
  
  filename: (req, file, cb) => {
    // 生成安全的文件名
    const ext = path.extname(file.originalname).toLowerCase()
    const safeName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${ext}`
    cb(null, safeName)
  }
})

// ==================== 文件过滤器 ====================

const fileFilter = (req, file, cb) => {
  // 检查 MIME 类型
  if (!UPLOAD_CONFIG.allowedMimes.includes(file.mimetype)) {
    return cb(new Error('不支持的文件类型'), false)
  }
  
  // 检查扩展名
  const ext = path.extname(file.originalname).toLowerCase()
  if (!UPLOAD_CONFIG.allowedExtensions.includes(ext)) {
    return cb(new Error('不支持的文件扩展名'), false)
  }
  
  cb(null, true)
}

// ==================== Multer 配置 ====================

const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: UPLOAD_CONFIG.maxSize,
    files: 5,
    fields: 10,
    fieldNameSize: 100
  }
})

// ==================== 上传路由 ====================

// 单文件上传
app.post('/upload/single', 
  upload.single('file'),
  async (req, res) => {
    if (!req.file) {
      return res.status(400).json({ error: '请选择文件' })
    }
    
    // 验证文件内容
    const isValid = await verifyFileContent(req.file)
    if (!isValid) {
      await fs.unlink(req.file.path) // 删除无效文件
      return res.status(400).json({ error: '文件内容无效' })
    }
    
    // 扫描病毒(可选)
    // await scanForVirus(req.file.path)
    
    res.json({
      success: true,
      file: {
        filename: req.file.filename,
        size: req.file.size,
        mimetype: req.file.mimetype
      }
    })
  }
)

// 多文件上传
app.post('/upload/multiple',
  upload.array('files', 5),
  async (req, res) => {
    res.json({
      success: true,
      files: req.files.map(f => ({
        filename: f.filename,
        size: f.size
      }))
    })
  }
)

// ==================== 文件内容验证 ====================

async function verifyFileContent(file) {
  // 检查文件魔数(真实类型)
  const buffer = Buffer.alloc(8)
  const fd = await fs.open(file.path, 'r')
  await fd.read(buffer, 0, 8, 0)
  await fd.close()
  
  // 常见文件签名
  const signatures = {
    'image/jpeg': [0xFF, 0xD8, 0xFF],
    'image/png': [0x89, 0x50, 0x4E, 0x47],
    'image/gif': [0x47, 0x49, 0x46],
    'application/pdf': [0x25, 0x50, 0x44, 0x46]
  }
  
  const expected = signatures[file.mimetype]
  if (!expected) return true
  
  return expected.every((byte, i) => buffer[i] === byte)
}

// ==================== 文件下载安全 ====================

app.get('/download/:filename', async (req, res) => {
  const filename = path.basename(req.params.filename)
  const filepath = path.join(UPLOAD_CONFIG.uploadDir, filename)
  
  // 验证路径
  if (!filepath.startsWith(UPLOAD_CONFIG.uploadDir)) {
    return res.status(403).json({ error: '非法路径' })
  }
  
  // 检查文件是否存在
  try {
    await fs.access(filepath)
  } catch {
    return res.status(404).json({ error: '文件不存在' })
  }
  
  // 设置安全响应头
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)
  res.setHeader('X-Content-Type-Options', 'nosniff')
  
  res.sendFile(filepath)
})

生产环境安全检查清单

部署前检查

markdown
## 安全检查清单

### 认证与授权
- [ ] 使用 HTTPS(HSTS 配置)
- [ ] 密码使用 bcrypt 加密存储(salt rounds >= 10)
- [ ] JWT/Session 配置安全(过期时间、签名算法)
- [ ] 实施登录速率限制
- [ ] 敏感操作需要二次验证
- [ ] 权限最小化原则

### 输入验证
- [ ] 所有用户输入都经过验证
- [ ] 使用参数化查询/ORM
- [ ] 文件上传限制类型和大小
- [ ] 防止路径遍历
- [ ] 验证 Content-Type

### 安全头
- [ ] 使用 Helmet 中间件
- [ ] 配置 CSP(Content-Security-Policy)
- [ ] 设置 HSTS
- [ ] X-Frame-Options 设置
- [ ] 禁用不必要的 HTTP 方法

### 数据保护
- [ ] 敏感数据加密存储
- [ ] 使用环境变量管理密钥
- [ ] 数据库访问最小权限
- [ ] 定期备份数据
- [ ] 敏感数据脱敏显示

### 日志与监控
- [ ] 记录安全事件
- [ ] 日志不包含敏感信息
- [ ] 设置异常告警
- [ ] 定期审查日志
- [ ] 监控异常登录

### 依赖安全
- [ ] npm audit 无高危漏洞
- [ ] 锁定依赖版本(package-lock.json)
- [ ] 定期更新依赖
- [ ] 移除未使用的依赖
- [ ] 验证依赖来源

### 错误处理
- [ ] 生产环境隐藏详细错误
- [ ] 全局错误处理
- [ ] 异步错误捕获
- [ ] 优雅关闭服务
- [ ] 未捕获异常处理

### 其他
- [ ] 禁用调试模式
- [ ] 移除测试端点
- [ ] 使用非 root 用户运行
- [ ] 定期安全审计
- [ ] 实施渗透测试

安全配置脚本

javascript
// scripts/security-check.js

const { exec } = require('child_process')
const fs = require('fs')
const path = require('path')

async function securityCheck() {
  console.log('🔍 开始安全检查...\n')
  const errors = []
  const warnings = []
  
  // 1. 检查环境变量
  console.log('✓ 检查环境变量')
  const requiredEnv = [
    'JWT_ACCESS_SECRET',
    'JWT_REFRESH_SECRET',
    'DATABASE_URL',
    'ENCRYPTION_KEY'
  ]
  
  const missing = requiredEnv.filter(key => !process.env[key])
  if (missing.length > 0) {
    errors.push(`缺少环境变量: ${missing.join(', ')}`)
  }
  
  // 2. 检查 HTTPS
  console.log('✓ 检查 HTTPS 配置')
  if (process.env.NODE_ENV === 'production') {
    if (!process.env.SSL_KEY || !process.env.SSL_CERT) {
      warnings.push('生产环境建议配置 SSL 证书')
    }
  }
  
  // 3. 检查密钥强度
  console.log('✓ 检查密钥强度')
  const secrets = [
    { name: 'JWT_ACCESS_SECRET', value: process.env.JWT_ACCESS_SECRET },
    { name: 'JWT_REFRESH_SECRET', value: process.env.JWT_REFRESH_SECRET }
  ]
  
  for (const secret of secrets) {
    if (secret.value && secret.value.length < 32) {
      warnings.push(`${secret.name} 长度不足 32 字符`)
    }
  }
  
  // 4. npm audit
  console.log('✓ 运行 npm audit')
  try {
    await new Promise((resolve, reject) => {
      exec('npm audit --audit-level=high --json', (error, stdout) => {
        if (error) {
          try {
            const result = JSON.parse(stdout)
            const highVulns = result.metadata?.vulnerabilities?.high || 0
            const criticalVulns = result.metadata?.vulnerabilities?.critical || 0
            
            if (highVulns > 0 || criticalVulns > 0) {
              errors.push(`发现 ${criticalVulns} 个严重漏洞, ${highVulns} 个高危漏洞`)
            }
          } catch (e) {
            warnings.push('npm audit 解析失败')
          }
        }
        resolve()
      })
    })
  } catch (e) {
    warnings.push('npm audit 执行失败')
  }
  
  // 5. 检查敏感文件
  console.log('✓ 检查敏感文件')
  const sensitiveFiles = [
    '.env',
    'private.key',
    'credentials.json',
    'id_rsa'
  ]
  
  for (const file of sensitiveFiles) {
    if (fs.existsSync(path.join(process.cwd(), file))) {
      errors.push(`发现敏感文件: ${file},请确保已加入 .gitignore`)
    }
  }
  
  // 6. 检查 .gitignore
  console.log('✓ 检查 .gitignore')
  const gitignorePath = path.join(process.cwd(), '.gitignore')
  if (fs.existsSync(gitignorePath)) {
    const gitignore = fs.readFileSync(gitignorePath, 'utf8')
    const requiredPatterns = ['.env', 'node_modules', '*.log']
    
    for (const pattern of requiredPatterns) {
      if (!gitignore.includes(pattern)) {
        warnings.push(`.gitignore 缺少: ${pattern}`)
      }
    }
  } else {
    errors.push('缺少 .gitignore 文件')
  }
  
  // 7. 检查 package.json
  console.log('✓ 检查 package.json 配置')
  const packageJson = require('../package.json')
  
  if (!packageJson.engines?.node) {
    warnings.push('package.json 未指定 Node.js 版本')
  }
  
  // 输出结果
  console.log('\n' + '='.repeat(50))
  
  if (errors.length > 0) {
    console.log('\n❌ 错误:')
    errors.forEach(e => console.log(`  - ${e}`))
  }
  
  if (warnings.length > 0) {
    console.log('\n⚠️  警告:')
    warnings.forEach(w => console.log(`  - ${w}`))
  }
  
  if (errors.length === 0 && warnings.length === 0) {
    console.log('\n✅ 所有安全检查通过')
    process.exit(0)
  }
  
  if (errors.length > 0) {
    console.log('\n❌ 安全检查未通过,请修复上述错误')
    process.exit(1)
  }
  
  console.log('\n⚠️  安全检查通过,但存在警告')
  process.exit(0)
}

securityCheck().catch(console.error)

Node.js 安全编码规范

javascript
// ==================== 安全编码示例 ====================

// 1. 使用严格模式
'use strict'

// 2. 冻结敏感配置
const config = Object.freeze({
  API_KEY: process.env.API_KEY,
  DB_URL: process.env.DATABASE_URL
})

// 3. 使用 const/let 替代 var
const secret = process.env.JWT_SECRET
let counter = 0

// 4. 错误处理使用 try-catch
async function safeOperation() {
  try {
    const result = await riskyOperation()
    return result
  } catch (error) {
    logger.error('操作失败', { error: error.message })
    throw new AppError('操作失败', 500)
  }
}

// 5. 使用 Promise.allSettled 处理并发
async function parallelOperations() {
  const results = await Promise.allSettled([
    fetchUser(),
    fetchOrders(),
    fetchSettings()
  ])
  
  results.forEach((result, index) => {
    if (result.status === 'rejected') {
      logger.error(`操作 ${index} 失败`, result.reason)
    }
  })
}

// 6. 安全的 JSON 解析
function safeJsonParse(str) {
  try {
    return JSON.parse(str)
  } catch (error) {
    logger.error('JSON 解析失败')
    return null
  }
}

// 7. 类型检查
function validateType(value, type) {
  if (typeof value !== type) {
    throw new TypeError(`期望 ${type},得到 ${typeof value}`)
  }
  return value
}

// 8. 禁止危险函数
function safeEval(expression) {
  throw new Error('禁止使用 eval()')
}

// 9. 安全的正则表达式(避免 ReDoS)
function safeRegex(pattern, input) {
  // 设置超时
  const timeout = setTimeout(() => {
    throw new Error('正则匹配超时')
  }, 1000)
  
  const result = pattern.test(input)
  clearTimeout(timeout)
  return result
}

// 10. 使用加密安全的随机数
const crypto = require('crypto')

function generateToken(bytes = 32) {
  return crypto.randomBytes(bytes).toString('hex')
}

function generateUUID() {
  return crypto.randomUUID()
}

// 11. 安全的对象操作
function safeAssign(target, source) {
  const dangerous = ['__proto__', 'constructor', 'prototype']
  const result = { ...target }
  
  for (const key of Object.keys(source)) {
    if (!dangerous.includes(key)) {
      result[key] = source[key]
    }
  }
  
  return result
}

// 12. 超时包装
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('操作超时')), ms)
    )
  ])
}

常见问题解答

Q1: 如何防止 SQL 注入?

javascript
// ❌ 危险:直接拼接
const sql = `SELECT * FROM users WHERE id = ${userId}`

// ✅ 安全:参数化查询
const [rows] = await pool.execute(
  'SELECT * FROM users WHERE id = ?',
  [userId]
)

// ✅ 安全:ORM 查询
const user = await User.findOne({ where: { id: userId } })

Q2: 如何安全地存储密码?

javascript
// ✅ 使用 bcrypt
const bcrypt = require('bcrypt')
const SALT_ROUNDS = 12

// 存储
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS)

// 验证
const isValid = await bcrypt.compare(password, hashedPassword)

Q3: 如何防止 XSS 攻击?

javascript
// 1. 输入验证
const { body } = require('express-validator')
body('content').trim().escape()

// 2. 输出编码
const escape = require('escape-html')
res.send(escape(userInput))

// 3. CSP 头
app.use(helmet.contentSecurityPolicy({
  directives: { defaultSrc: ["'self'"] }
}))

// 4. httpOnly Cookie
res.cookie('sessionId', id, { httpOnly: true })

Q4: 如何实现安全的 API 认证?

javascript
// JWT + 刷新令牌方案
const tokens = generateTokens(user)

// AccessToken: 短期有效,存内存
// RefreshToken: 长期有效,存 HttpOnly Cookie 或安全存储

// 刷新端点
app.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken
  
  if (!refreshToken) {
    return res.status(401).json({ error: '缺少刷新令牌' })
  }
  
  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET)
    const user = await User.findById(decoded.userId)
    
    // 生成新的 AccessToken
    const accessToken = jwt.sign(
      { userId: user._id },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    )
    
    res.json({ accessToken })
  } catch (e) {
    res.status(401).json({ error: '无效的刷新令牌' })
  }
})

Q5: 如何防止 CSRF 攻击?

javascript
// 1. SameSite Cookie
res.cookie('sessionId', id, { sameSite: 'strict' })

// 2. CSRF Token
const csrf = require('csurf')
app.use(csrf({ cookie: true }))

// 前端获取 Token
app.get('/csrf-token', (req, res) => {
  res.json({ csrfToken: req.csrfToken() })
})

// 前端请求时携带
fetch('/api/data', {
  headers: { 'X-CSRF-Token': csrfToken }
})

// 3. 检查 Origin/Referer
app.use((req, res, next) => {
  const origin = req.get('Origin') || req.get('Referer')
  if (origin && !allowedOrigins.includes(new URL(origin).origin)) {
    return res.status(403).json({ error: 'CSRF 检测' })
  }
  next()
})

参考资源

官方文档

安全工具

学习资源


Node.js 22+ 安全新特性

权限模型

Node.js 22.13+ 权限模型已稳定,可限制应用对文件系统和网络的访问:

bash
# 限制文件系统和网络访问
node --permission \
  --allow-fs-read=/app/data \
  --allow-fs-read=/app/config \
  --allow-fs-write=/app/logs \
  --allow-fs-write=/app/uploads \
  --allow-net=0.0.0.0:3000 \
  --allow-net=db.internal:5432 \
  app.js
javascript
// 运行时检查权限
if (process.permission.has('fs.read', '/etc/passwd')) {
  console.error('安全警告:应用不应读取系统文件')
}

原生 crypto.randomUUID

无需导入 crypto 模块即可生成 UUID:

javascript
// Node.js 22+ 全局可用
const sessionId = crypto.randomUUID()
const csrfToken = crypto.randomUUID()

Web Crypto API

Node.js 22+ 全局 crypto.subtle 可用,与浏览器 API 一致:

javascript
// 生成安全随机值
const salt = crypto.getRandomValues(new Uint8Array(16))

// SHA-256 哈希
const encoder = new TextEncoder()
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(password))
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')

// AES-GCM 加密
const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  true,
  ['encrypt', 'decrypt']
)
const iv = crypto.getRandomValues(new Uint8Array(12))
const encrypted = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv },
  key,
  encoder.encode(sensitiveData)
)

--env-file 避免密钥泄露

bash
# 替代 dotenv,避免密钥硬编码
node --env-file=.env.production app.js

# .env.production(不提交到 Git)
JWT_SECRET=your-256-bit-secret
DATABASE_URL=postgresql://user:pass@host:5432/db
ENCRYPTION_KEY=your-encryption-key