引言

随着Node.js在后端开发中的广泛应用,安全问题变得越来越重要。一个安全漏洞可能导致数据泄露、服务中断甚至企业声誉受损。本文将深入探讨Node.js安全开发的最佳实践,帮助你构建安全、可靠的Web应用。

OWASP Top 10防护

1. 注入攻击防护

SQL注入防护

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ❌ 不安全的查询
const query = `SELECT * FROM users WHERE email = '${email}'`
db.query(query)

// ✅ 使用参数化查询
const mysql = require('mysql2/promise')

async function getUser(email) {
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'myapp'
  })

  const [rows] = await connection.execute(
    'SELECT * FROM users WHERE email = ?',
    [email]
  )

  await connection.end()
  return rows[0]
}

// 使用ORM
const { Sequelize, DataTypes } = require('sequelize')

const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: 'mysql'
})

const User = sequelize.define('User', {
  email: DataTypes.STRING,
  password: DataTypes.STRING
})

// 安全查询
const user = await User.findOne({ where: { email } })

NoSQL注入防护

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 不安全的MongoDB查询
const query = { email: req.body.email }
db.users.findOne(query)

// ✅ 使用类型检查和过滤
const { ObjectId } = require('mongodb')
const { body, validationResult } = require('express-validator')

async function findUser(req, res) {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() })
  }

  const { email } = req.body

  // 验证邮箱格式
  if (!validator.isEmail(email)) {
    return res.status(400).json({ error: 'Invalid email format' })
  }

  const user = await db.users.findOne({ email })
  res.json(user)
}

2. 身份验证和授权

JWT实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const { promisify } = require('util')

const signToken = promisify(jwt.sign)
const verifyToken = promisify(jwt.verify)

// JWT配置
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'

// 生成Token
async function generateToken(payload) {
  return await signToken(payload, JWT_SECRET, {
    expiresIn: JWT_EXPIRES_IN,
    issuer: 'your-app',
    audience: 'your-users'
  })
}

// 验证Token中间件
async function authenticateToken(req, res, next) {
  try {
    const authHeader = req.headers['authorization']
    const token = authHeader && authHeader.split(' ')[1]

    if (!token) {
      return res.status(401).json({ error: 'Access token required' })
    }

    const decoded = await verifyToken(token, JWT_SECRET)
    req.user = decoded
    next()
  } catch (error) {
    return res.status(403).json({ error: 'Invalid or expired token' })
  }
}

// 角色授权中间件
function authorize(roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' })
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' })
    }

    next()
  }
}

// 使用示例
app.get('/admin/users',
  authenticateToken,
  authorize(['admin']),
  async (req, res) => {
    // 管理员功能
  }
)

密码安全处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const bcrypt = require('bcrypt')
const crypto = require('crypto')

// 密码哈希
async function hashPassword(password) {
  const saltRounds = 12
  return await bcrypt.hash(password, saltRounds)
}

// 验证密码
async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash)
}

// 密码重置Token
function generateResetToken() {
  return crypto.randomBytes(32).toString('hex')
}

// 密码验证规则
const passwordPolicy = {
  minLength: 8,
  requireUppercase: true,
  requireLowercase: true,
  requireNumbers: true,
  requireSpecialChars: true
}

function validatePassword(password) {
  const errors = []

  if (password.length < passwordPolicy.minLength) {
    errors.push(`Password must be at least ${passwordPolicy.minLength} characters`)
  }

  if (passwordPolicy.requireUppercase && !/[A-Z]/.test(password)) {
    errors.push('Password must contain uppercase letters')
  }

  if (passwordPolicy.requireLowercase && !/[a-z]/.test(password)) {
    errors.push('Password must contain lowercase letters')
  }

  if (passwordPolicy.requireNumbers && !/\d/.test(password)) {
    errors.push('Password must contain numbers')
  }

  if (passwordPolicy.requireSpecialChars && !/[!@#$%^&*]/.test(password)) {
    errors.push('Password must contain special characters')
  }

  return errors
}

3. 数据加密

敏感数据加密

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const crypto = require('crypto')

class EncryptionService {
  constructor(algorithm = 'aes-256-gcm', secretKey = process.env.ENCRYPTION_KEY) {
    this.algorithm = algorithm
    this.secretKey = Buffer.from(secretKey, 'hex')
    this.ivLength = 16
    this.tagLength = 16
  }

  encrypt(text) {
    const iv = crypto.randomBytes(this.ivLength)
    const cipher = crypto.createCipher(this.algorithm, this.secretKey)
    cipher.setAAD(Buffer.from('additional-data'))

    let encrypted = cipher.update(text, 'utf8', 'hex')
    encrypted += cipher.final('hex')

    const tag = cipher.getAuthTag()

    return {
      iv: iv.toString('hex'),
      encryptedData: encrypted,
      tag: tag.toString('hex')
    }
  }

  decrypt(encryptedData) {
    const decipher = crypto.createDecipher(this.algorithm, this.secretKey)
    decipher.setAAD(Buffer.from('additional-data'))
    decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex'))

    let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8')
    decrypted += decipher.final('utf8')

    return decrypted
  }
}

// 使用示例
const encryption = new EncryptionService()

// 加密用户敏感信息
const userData = {
  name: 'John Doe',
  email: 'john@example.com',
  ssn: encryption.encrypt('123-45-6789')
}

// 解密
const decryptedSSN = encryption.decrypt(userData.ssn)

HTTPS配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const https = require('https')
const fs = require('fs')

// HTTPS服务器配置
const httpsOptions = {
  key: fs.readFileSync('/path/to/private.key'),
  cert: fs.readFileSync('/path/to/certificate.crt'),
  ca: fs.readFileSync('/path/to/ca_bundle.crt'),
  minVersion: 'TLSv1.2',
  ciphers: [
    'ECDHE-ECDSA-AES256-GCM-SHA384',
    'ECDHE-RSA-AES256-GCM-SHA384',
    'ECDHE-ECDSA-CHACHA20-POLY1305',
    'ECDHE-RSA-CHACHA20-POLY1305',
    'ECDHE-ECDSA-AES128-GCM-SHA256',
    'ECDHE-RSA-AES128-GCM-SHA256'
  ].join(':'),
  honorCipherOrder: true
}

const app = express()
const server = https.createServer(httpsOptions, app)

// HSTS中间件
app.use((req, res, next) => {
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  )
  next()
})

4. 安全头部配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const helmet = require('helmet')

// 基础安全头部
app.use(helmet())

// 自定义安全头部
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.trusted.com"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
      manifestSrc: ["'self'"],
      workerSrc: ["'self'"]
    }
  },
  crossOriginEmbedderPolicy: false,
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}))

// CORS配置
const cors = require('cors')
const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://example.com',
      'https://app.example.com'
    ]

    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true,
  optionsSuccessStatus: 200
}

app.use(cors(corsOptions))

输入验证和清理

Express-validator集成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const { body, param, query, validationResult } = require('express-validator')
const { sanitizeBody } = require('express-validator')

// 用户注册验证
const validateUserRegistration = [
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Valid email required'),

  body('password')
    .isLength({ min: 8 })
    .withMessage('Password must be at least 8 characters')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
    .withMessage('Password must include uppercase, lowercase, number and special character'),

  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .escape()
    .withMessage('Name must be between 2 and 50 characters'),

  body('age')
    .optional()
    .isInt({ min: 13, max: 120 })
    .withMessage('Age must be between 13 and 120')
]

// 清理输入数据
const sanitizeInput = [
  sanitizeBody('bio').escape(),
  sanitizeBody('website').escape(),
  sanitizeBody('location').escape()
]

// 验证结果处理中间件
function handleValidationErrors(req, res, next) {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'Validation failed',
      details: errors.array()
    })
  }
  next()
}

// 使用示例
app.post('/api/users',
  validateUserRegistration,
  sanitizeInput,
  handleValidationErrors,
  async (req, res) => {
    // 处理用户注册
  }
)

XSS防护

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const xss = require('xss')
const validator = require('validator')

// XSS过滤器配置
const xssOptions = {
  whiteList: {
    a: ['href', 'title', 'target'],
    b: [],
    br: [],
    em: [],
    strong: [],
    i: [],
    li: [],
    ol: [],
    p: [],
    ul: []
  },
  stripIgnoreTag: true,
  stripIgnoreTagBody: ['script']
}

// 清理用户输入
function sanitizeInput(input) {
  if (typeof input === 'string') {
    return xss(validator.escape(input), xssOptions)
  }
  if (typeof input === 'object' && input !== null) {
    const sanitized = {}
    for (const [key, value] of Object.entries(input)) {
      sanitized[key] = sanitizeInput(value)
    }
    return sanitized
  }
  return input
}

// 中间件
function sanitizeRequest(req, res, next) {
  req.body = sanitizeInput(req.body)
  req.query = sanitizeInput(req.query)
  req.params = sanitizeInput(req.params)
  next()
}

app.use(sanitizeRequest)

文件上传安全

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const multer = require('multer')
const path = require('path')
const fs = require('fs')

// 文件过滤器
const fileFilter = (req, file, cb) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']

  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true)
  } else {
    cb(new Error('Invalid file type'), false)
  }
}

// 存储配置
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads/'
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true })
    }
    cb(null, uploadDir)
  },
  filename: (req, file, cb) => {
    // 生成唯一文件名
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
    cb(null, uniqueSuffix + path.extname(file.originalname))
  }
})

const upload = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
    files: 1
  }
})

// 病毒扫描集成
const ClamScan = require('clamscan')

async function scanFile(filePath) {
  try {
    const clamscan = await new ClamScan().init()
    const scanResult = await clamscan.scanFile(filePath)
    return scanResult.isInfected
  } catch (error) {
    console.error('Virus scan error:', error)
    return true // 保守处理,认为可能感染
  }
}

// 文件上传路由
app.post('/upload', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file uploaded' })
    }

    // 病毒扫描
    const isInfected = await scanFile(req.file.path)
    if (isInfected) {
      fs.unlinkSync(req.file.path)
      return res.status(400).json({ error: 'File contains virus' })
    }

    res.json({
      message: 'File uploaded successfully',
      file: {
        filename: req.file.filename,
        originalName: req.file.originalname,
        size: req.file.size
      }
    })
  } catch (error) {
    if (req.file) {
      fs.unlinkSync(req.file.path)
    }
    res.status(500).json({ error: 'Upload failed' })
  }
})

依赖安全

依赖漏洞扫描

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 使用npm audit
{
  "scripts": {
    "audit": "npm audit",
    "audit:fix": "npm audit fix",
    "security-check": "npm audit --audit-level moderate"
  }
}

// 使用Snyk进行安全扫描
const snyk = require('snyk')

async function checkVulnerabilities() {
  try {
    await snyk.test()
    console.log('No vulnerabilities found')
  } catch (error) {
    console.error('Vulnerabilities detected:', error)
    process.exit(1)
  }
}

// CI/CD集成
if (process.env.CI) {
  checkVulnerabilities()
}

安全的依赖管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# package.json配置
{
  "engines": {
    "node": ">=14.0.0",
    "npm": ">=6.0.0"
  },
  "dependencies": {
    "express": "4.17.1",  // 锁定版本
    "lodash": "^4.17.20"   // 允许补丁更新
  },
  "overrides": {
    "lodash": {
      "node": "^14.17.0"   // 覆盖子依赖
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 运行时依赖验证
const { execSync } = require('child_process')
const semver = require('semver')

function checkNodeVersion() {
  const nodeVersion = process.version
  const minVersion = require('../package.json').engines.node.replace('>=', '')

  if (!semver.satisfies(nodeVersion, minVersion)) {
    console.error(`Node.js version ${nodeVersion} is not supported. Please use ${minVersion}`)
    process.exit(1)
  }
}

function checkDependencies() {
  try {
    const output = execSync('npm ls --depth=0', { encoding: 'utf8' })
    const lines = output.split('\n')

    for (const line of lines) {
      if (line.includes('UNMET DEPENDENCY')) {
        throw new Error('Unmet dependencies detected')
      }
    }
  } catch (error) {
    console.error('Dependency check failed:', error.message)
    process.exit(1)
  }
}

// 应用启动时检查
checkNodeVersion()
checkDependencies()

错误处理和日志

安全的错误处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 统一错误处理中间件
function errorHandler(err, req, res, next) {
  // 记录错误(不暴露敏感信息)
  console.error('Error:', {
    message: err.message,
    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
    timestamp: new Date().toISOString(),
    requestId: req.id,
    path: req.path,
    method: req.method
  })

  // 不暴露内部错误详情
  let statusCode = 500
  let message = 'Internal Server Error'

  if (err.name === 'ValidationError') {
    statusCode = 400
    message = err.message
  } else if (err.name === 'UnauthorizedError') {
    statusCode = 401
    message = 'Unauthorized'
  } else if (err.name === 'ForbiddenError') {
    statusCode = 403
    message = 'Forbidden'
  } else if (err.name === 'NotFoundError') {
    statusCode = 404
    message = 'Resource not found'
  }

  res.status(statusCode).json({
    error: message,
    requestId: req.id,
    timestamp: new Date().toISOString()
  })
}

app.use(errorHandler)

安全日志记录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const winston = require('winston')
const { requestId } = require('./middleware/requestId')

// 日志配置
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'api-service' },
  transports: [
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error'
    }),
    new winston.transports.File({
      filename: 'logs/combined.log'
    })
  ]
})

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }))
}

// 安全事件日志
function logSecurityEvent(event, req, details = {}) {
  logger.warn('Security Event', {
    event,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    requestId: req.id,
    timestamp: new Date().toISOString(),
    ...details
  })
}

// 使用示例
app.use((req, res, next) => {
  req.id = Math.random().toString(36).substr(2, 9)

  // 记录敏感操作
  if (req.path.includes('/admin') || req.path.includes('/auth')) {
    logSecurityEvent('Sensitive Access', req, {
      path: req.path,
      method: req.method
    })
  }

  next()
})

会话和Cookie安全

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const session = require('express-session')
const cookieParser = require('cookie-parser')
const MongoStore = require('connect-mongo')(session)

// 会话配置
const sessionConfig = {
  name: 'sessionId', // 避免默认名称
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: new MongoStore({
    url: process.env.MONGODB_URI,
    collection: 'sessions'
  }),
  cookie: {
    httpOnly: true, // 防止XSS
    secure: process.env.NODE_ENV === 'production', // HTTPS only
    sameSite: 'strict', // CSRF防护
    maxAge: 24 * 60 * 60 * 1000 // 24小时
  }
}

app.use(session(sessionConfig))

// 速率限制
const rateLimit = require('express-rate-limit')

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 5, // 限制5次尝试
  skipSuccessfulRequests: true,
  handler: (req, res) => {
    logSecurityEvent('Rate Limit Exceeded', req, {
      endpoint: '/login',
      attempts: 5
    })
    res.status(429).json({ error: 'Too many login attempts' })
  }
})

app.post('/login', loginLimiter, async (req, res) => {
  // 登录逻辑
})

总结

Node.js应用安全需要多层次的防护策略:

基础防护:

  1. 输入验证和清理
  2. 输出编码和XSS防护
  3. SQL/NoSQL注入防护
  4. 文件上传安全

认证和授权:

  1. 强密码策略
  2. 安全的会话管理
  3. JWT令牌安全
  4. 角色权限控制

数据保护:

  1. HTTPS加密传输
  2. 敏感数据加密存储
  3. 安全头部配置
  4. 依赖漏洞管理

监控和响应:

  1. 安全事件日志
  2. 错误处理机制
  3. 速率限制
  4. 安全审计

遵循这些最佳实践,可以大大提高Node.js应用的安全性,保护应用和用户数据免受攻击。


相关资源: