安全

应用安全加固完全指南:XSS、CSRF、SQL 注入防护与认证授权

深入学习 Web 应用安全,掌握常见漏洞防护、加密、认证授权、安全审计等实战技术

18 分钟阅读
#应用安全 #XSS #CSRF #认证 #加密

📖 文章概述

安全是所有应用的基础。本文讲解如何防护常见漏洞、实现认证授权、加密敏感数据,以及构建安全的应用架构。


🎯 安全威胁模型

OWASP Top 10

1. 注入漏洞 (Injection)
   - SQL 注入、命令注入、LDAP 注入
   
2. 失效的身份验证 (Broken Authentication)
   - 弱密码、密钥泄露、会话管理不当
   
3. 敏感数据暴露 (Sensitive Data Exposure)
   - 未加密传输、弱加密、敏感信息日志记录
   
4. 外部实体注入 (XML External Entities, XXE)
   - XML 外部实体引用、XML 炸弹
   
5. 失效的访问控制 (Broken Access Control)
   - 权限提升、水平越权、垂直越权
   
6. 安全配置缺陷 (Security Misconfiguration)
   - 默认密码、不必要的服务、过度权限
   
7. 跨站脚本 (XSS)
   - 反射型、存储型、DOM 型 XSS
   
8. 不安全的反序列化 (Insecure Deserialization)
   - 恶意对象注入、远程代码执行
   
9. 使用含有已知漏洞的组件 (Using Components with Known Vulnerabilities)
   - 过期依赖、未打补丁库
   
10. 日志记录和监控不足 (Insufficient Logging & Monitoring)
    - 无法检测攻击、事件记录不完整

🛡️ 防护常见漏洞

1. XSS(跨站脚本)防护

// ❌ 不安全 - 直接输出用户输入
app.get('/vulnerable', (req, res) => {
  const search = req.query.q
  res.send(`<p>搜索结果: ${search}</p>`)  // XSS 风险
})

// ✅ 安全 - HTML 转义
const escapeHtml = (unsafe) => {
  return unsafe
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

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

// 使用专门库
import DOMPurify from 'isomorphic-dompurify'

const cleanHtml = DOMPurify.sanitize(userInput, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
  ALLOWED_ATTR: ['href']
})

// Vue.js 自动转义
// <template>
//   <!-- 自动转义 -->
//   <p>{{ userInput }}</p>
//   
//   <!-- 需要 HTML 时使用 v-html(谨慎)-->
//   <p v-html="sanitizedHTML"></p>
// </template>

// React 自动转义
// <p>{userInput}</p>  // 安全
// <p dangerouslySetInnerHTML={{ __html: userInput }} />  // 需谨慎

2. CSRF(跨站请求伪造)防护

import csrf from 'csurf'
import cookieParser from 'cookie-parser'
import session from 'express-session'

// 配置 CSRF 防护
app.use(cookieParser())
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,      // 仅 HTTPS
    httpOnly: true,    // 无法通过 JS 访问
    sameSite: 'strict'  // 仅同站请求
  }
}))

// CSRF 令牌中间件
const csrfProtection = csrf({ cookie: false })

// 为表单添加 CSRF 令牌
app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() })
})

// 验证 CSRF 令牌
app.post('/submit', csrfProtection, (req, res) => {
  // CSRF 令牌已验证
  res.json({ message: '表单提交成功' })
})

// 前端提交表单
// <form method="POST" action="/submit">
//   <input type="hidden" name="_csrf" value="<%= csrfToken %>">
//   <input type="text" name="username">
//   <button type="submit">提交</button>
// </form>

3. SQL 注入防护

import pg from 'pg'

const pool = new pg.Pool()

// ❌ 不安全 - 字符串拼接
// const query = `SELECT * FROM users WHERE id = ${userId}`
// 攻击: userId = "1 OR 1=1"

// ✅ 安全 - 参数化查询
app.get('/users/:id', async (req, res) => {
  const { id } = req.params
  
  try {
    // 使用参数化查询
    const result = await pool.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    )
    
    res.json(result.rows)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// ORM 防护(Prisma)
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

app.get('/users/:id', async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: parseInt(req.params.id) }
  })
  res.json(user)
})

// 输入验证
const validateUserId = (id) => {
  if (!Number.isInteger(parseInt(id))) {
    throw new Error('无效的用户 ID')
  }
  if (parseInt(id) < 0 || parseInt(id) > 999999) {
    throw new Error('用户 ID 超出范围')
  }
  return parseInt(id)
}

app.get('/users/:id', async (req, res) => {
  try {
    const id = validateUserId(req.params.id)
    const user = await prisma.user.findUnique({
      where: { id }
    })
    res.json(user)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

4. 命令注入防护

import { execFile } from 'child_process'

// ❌ 不安全 - 使用 exec(不建议)
// exec(`ls ${userDir}`)  // 攻击: userDir = "; rm -rf /"

// ✅ 安全 - 使用 execFile(参数分离)
const safeExecute = (command, args) => {
  return new Promise((resolve, reject) => {
    execFile(command, args, (error, stdout, stderr) => {
      if (error) reject(error)
      resolve(stdout)
    })
  })
}

// 使用
try {
  const result = await safeExecute('ls', [userDir])
  res.json({ files: result })
} catch (error) {
  res.status(500).json({ error: error.message })
}

// 避免执行 shell 命令
// 改用专门的库或 API
import fs from 'fs'

// 不执行命令,直接使用 Node.js API
const files = fs.readdirSync(userDir)

🔐 认证和授权

5. JWT 认证实现

import jwt from 'jsonwebtoken'

const JWT_SECRET = process.env.JWT_SECRET
const JWT_EXPIRY = '24h'

// 生成 token
function generateToken(userId, email) {
  return jwt.sign(
    {
      userId,
      email,
      iat: Math.floor(Date.now() / 1000)
    },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRY }
  )
}

// 验证 token
function verifyToken(token) {
  try {
    return jwt.verify(token, JWT_SECRET)
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new Error('Token 已过期')
    }
    throw new Error('无效的 Token')
  }
}

// 刷新 token(使用 Refresh Token)
function generateRefreshToken(userId) {
  return jwt.sign(
    { userId, type: 'refresh' },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  )
}

// 认证中间件
const authMiddleware = (req, res, next) => {
  const authHeader = req.headers.authorization
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: '缺少认证令牌' })
  }
  
  const token = authHeader.slice(7)
  
  try {
    req.user = verifyToken(token)
    next()
  } catch (error) {
    res.status(401).json({ error: error.message })
  }
}

// 使用
app.post('/login', async (req, res) => {
  const { email, password } = req.body
  
  // 验证密码
  const user = await User.findOne({ email })
  if (!user || !await user.comparePassword(password)) {
    return res.status(401).json({ error: '用户名或密码错误' })
  }
  
  const token = generateToken(user.id, user.email)
  const refreshToken = generateRefreshToken(user.id)
  
  res.json({ token, refreshToken })
})

app.get('/profile', authMiddleware, (req, res) => {
  res.json({ userId: req.user.userId })
})

6. 基于角色的访问控制(RBAC)

// 定义角色和权限
const permissions = {
  'admin': ['read', 'write', 'delete', 'manage_users'],
  'editor': ['read', 'write'],
  'viewer': ['read']
}

// 权限检查中间件
const requirePermission = (requiredPermission) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: '未认证' })
    }
    
    const userRole = req.user.role
    const userPermissions = permissions[userRole]
    
    if (!userPermissions || !userPermissions.includes(requiredPermission)) {
      return res.status(403).json({ error: '权限不足' })
    }
    
    next()
  }
}

// 使用
app.delete('/users/:id', 
  authMiddleware,
  requirePermission('delete'),
  async (req, res) => {
    await User.findByIdAndDelete(req.params.id)
    res.json({ message: '用户已删除' })
  }
)

// 基于属性的访问控制(ABAC)
const checkAccess = (resource, action, user) => {
  // 所有者总是可以编辑自己的资源
  if (resource.owner === user.id && ['read', 'write'].includes(action)) {
    return true
  }
  
  // 管理员可以执行任何操作
  if (user.role === 'admin') {
    return true
  }
  
  // 其他情况拒绝
  return false
}

app.put('/posts/:id', authMiddleware, async (req, res) => {
  const post = await Post.findById(req.params.id)
  
  if (!checkAccess(post, 'write', req.user)) {
    return res.status(403).json({ error: '权限不足' })
  }
  
  await post.updateOne(req.body)
  res.json(post)
})

🔑 加密和密钥管理

7. 数据加密

import crypto from 'crypto'

// AES 加密(对称加密)
const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex')
const IV_LENGTH = 16

function encrypt(text) {
  const iv = crypto.randomBytes(IV_LENGTH)
  const cipher = crypto.createCipheriv(
    'aes-256-cbc',
    ENCRYPTION_KEY,
    iv
  )
  
  let encrypted = cipher.update(text, 'utf-8', 'hex')
  encrypted += cipher.final('hex')
  
  // 返回 IV + 密文
  return iv.toString('hex') + ':' + encrypted
}

function decrypt(text) {
  const parts = text.split(':')
  const iv = Buffer.from(parts[0], 'hex')
  const decipher = crypto.createDecipheriv(
    'aes-256-cbc',
    ENCRYPTION_KEY,
    iv
  )
  
  let decrypted = decipher.update(parts[1], 'hex', 'utf-8')
  decrypted += decipher.final('utf-8')
  
  return decrypted
}

// 密码哈希(单向)
import bcrypt from 'bcrypt'

async function hashPassword(password) {
  const salt = await bcrypt.genSalt(10)  // 成本因子
  return bcrypt.hash(password, salt)
}

async function comparePassword(password, hash) {
  return bcrypt.compare(password, hash)
}

// 使用
const hashedPassword = await hashPassword(userPassword)
await User.create({ email, password: hashedPassword })

// 验证
const isValid = await comparePassword(inputPassword, user.password)

8. 安全的密钥管理

// ✅ 推荐 - 使用环境变量和密钥管理服务
class KeyManager {
  constructor() {
    // 从环境变量加载主密钥
    this.masterKey = process.env.MASTER_KEY
    
    // 使用 AWS Secrets Manager / HashiCorp Vault
    this.secretsClient = initializeSecretsManager()
  }
  
  async getKey(keyName) {
    // 缓存在内存中(不推荐敏感密钥)
    if (this.keyCache && this.keyCache[keyName]) {
      return this.keyCache[keyName]
    }
    
    // 从密钥管理服务获取
    const key = await this.secretsClient.getSecret(keyName)
    
    if (!this.keyCache) this.keyCache = {}
    this.keyCache[keyName] = key
    
    return key
  }
  
  async rotateKey(keyName) {
    // 生成新密钥
    const newKey = crypto.randomBytes(32).toString('hex')
    
    // 保存新密钥
    await this.secretsClient.putSecret(keyName, newKey)
    
    // 更新缓存
    if (this.keyCache) delete this.keyCache[keyName]
    
    console.log(`密钥 ${keyName} 已轮换`)
  }
}

// ❌ 不安全 - 密钥在代码中硬编码
// const SECRET_KEY = 'my-secret-key'

// ✅ 安全 - 使用环境变量
const SECRET_KEY = process.env.SECRET_KEY
if (!SECRET_KEY) {
  throw new Error('SECRET_KEY 环境变量未设置')
}

🔒 安全的传输和存储

9. HTTPS 和安全头

import helmet from 'helmet'
import https from 'https'
import fs from 'fs'

// 使用 Helmet 设置安全头
app.use(helmet())

// 详细配置
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],  // 仅在开发环境
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
    upgradeInsecureRequests: []
  }
}))

// 其他安全头
app.use(helmet.hsts({
  maxAge: 31536000,  // 1 年
  includeSubDomains: true,
  preload: true
}))

app.use(helmet.noSniff())          // 禁止 MIME 嗅探
app.use(helmet.xssFilter())        // 启用 XSS 过滤
app.use(helmet.referrerPolicy({    // 控制 Referer 头
  policy: 'strict-origin-when-cross-origin'
}))

// HTTPS 配置
const options = {
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert')
}

https.createServer(options, app).listen(443)

// 强制 HTTP 重定向到 HTTPS
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(301, `https://${req.header('host')}${req.url}`)
  } else {
    next()
  }
})
import cookieParser from 'cookie-parser'

app.use(cookieParser())

// 设置安全 Cookie
app.use((req, res, next) => {
  res.cookie('sessionId', generateSessionId(), {
    secure: true,           // 仅 HTTPS 传输
    httpOnly: true,         // 禁止 JavaScript 访问
    sameSite: 'strict',     // 防止 CSRF
    maxAge: 3600000,        // 1 小时
    path: '/',              // 仅在根路径
    domain: 'example.com'   // 指定域名
  })
  
  next()
})

// ❌ 不安全
// res.cookie('sessionId', sessionId)

// ✅ 安全
res.cookie('sessionId', sessionId, {
  secure: true,
  httpOnly: true,
  sameSite: 'strict'
})

🔍 安全审计和监控

11. 安全日志记录

class SecurityAuditLog {
  async logSecurityEvent(event) {
    const auditEntry = {
      timestamp: new Date(),
      eventType: event.type,  // login, logout, access_denied, etc.
      userId: event.userId,
      ipAddress: event.ipAddress,
      userAgent: event.userAgent,
      resource: event.resource,
      action: event.action,
      result: event.result,   // success, failure
      details: event.details
    }
    
    // 保存到数据库
    await AuditLog.create(auditEntry)
    
    // 在受监控的 SIEM 系统中记录敏感事件
    if (['failed_login', 'privilege_escalation', 'access_denied'].includes(event.type)) {
      await this.alertSecurityTeam(auditEntry)
    }
  }
  
  async alertSecurityTeam(event) {
    // 发送告警
    await sendAlert({
      channel: 'security',
      severity: 'high',
      message: `安全事件: ${event.eventType} - ${event.userId}`
    })
  }
}

// 使用
const auditLog = new SecurityAuditLog()

app.post('/login', async (req, res) => {
  const { email, password } = req.body
  
  try {
    const user = await User.findOne({ email })
    const isValid = user && await user.comparePassword(password)
    
    if (isValid) {
      await auditLog.logSecurityEvent({
        type: 'login',
        userId: user.id,
        ipAddress: req.ip,
        userAgent: req.get('user-agent'),
        result: 'success'
      })
      
      res.json({ token: generateToken(user.id) })
    } else {
      await auditLog.logSecurityEvent({
        type: 'failed_login',
        ipAddress: req.ip,
        userAgent: req.get('user-agent'),
        result: 'failure',
        details: { email }
      })
      
      res.status(401).json({ error: '用户名或密码错误' })
    }
  } catch (error) {
    res.status(500).json({ error: '内部错误' })
  }
})

12. 依赖漏洞扫描

# npm audit - 检查漏洞
npm audit

# 修复漏洞
npm audit fix

# 自动修复不兼容的版本
npm audit fix --force

# 生成审计报告
npm audit --json > audit-report.json
// 在 CI/CD 流水线中集成
// .github/workflows/security.yml
name: Security Audit

on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '18'
      - run: npm install
      - run: npm audit --audit-level=moderate

🎓 安全最佳实践清单

DO ✅

□ 使用参数化查询防止 SQL 注入
□ 对所有用户输入进行验证和转义
□ 使用 HTTPS 加密所有通信
□ 设置安全的 Cookie 标志(HttpOnly, Secure, SameSite)
□ 实现强认证(JWT, OAuth 2.0)
□ 使用 RBAC 进行访问控制
□ 记录安全事件进行审计
□ 定期更新依赖和补丁
□ 使用密钥管理服务存储敏感数据
□ 实现速率限制防止暴力破解
□ 定期进行安全审计和渗透测试

DON'T ❌

□ 不要在日志中记录敏感信息(密码、token)
□ 不要在代码中硬编码密钥
□ 不要使用过时的加密算法
□ 不要信任客户端验证
□ 不要暴露详细的错误信息
□ 不要使用弱密码策略
□ 不要忽视依赖漏洞
□ 不要禁用 HTTPS
□ 不要实现自己的加密算法

📚 总结

应用安全的核心:

  • 防护漏洞: XSS、CSRF、SQL 注入、命令注入
  • 强认证: JWT、OAuth 2.0、多因子认证
  • 访问控制: RBAC、ABAC、细粒度权限
  • 数据保护: 加密传输、加密存储、密钥管理
  • 监控审计: 安全日志、告警、定期扫描

安全是一个持续的过程,不是一次性的工作!