Skip to content
On this page

代码审查中的安全考虑

安全是软件开发中最重要的考虑因素之一。在代码审查过程中,必须仔细检查代码的安全性,以防止各种安全漏洞。本章将详细介绍在代码审查中如何识别和防范安全问题。

安全审查清单

在代码审查中,应重点关注以下安全相关问题:

1. 输入验证和净化

检查点:

  • 所有外部输入是否都经过验证?
  • 是否对输入长度、格式、类型进行了限制?
  • 是否对特殊字符进行了净化?

常见漏洞:

  • 注入攻击(SQL、NoSQL、OS命令、LDAP等)
  • XSS(跨站脚本攻击)
  • CSRF(跨站请求伪造)

2. 输出编码

检查点:

  • 是否对输出到HTML、JavaScript、CSS、URL的数据进行了适当的编码?
  • 是否使用了安全的模板引擎?

3. 认证和授权

检查点:

  • 用户身份是否得到验证?
  • 是否实现了适当的会话管理?
  • 权限控制是否正确实施?

4. 数据保护

检查点:

  • 敏感数据是否得到了适当加密?
  • 是否安全地处理了密码和个人信息?

注入攻击防护

1. SQL注入

Bad:

javascript
// 问题:直接拼接SQL语句
function getUser(username, password) {
  // 危险的SQL查询
  const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
  return db.query(query);
}

// 更危险的例子
app.get('/user/:id', (req, res) => {
  const userId = req.params.id;
  const query = `SELECT * FROM users WHERE id = ${userId}`;
  db.query(query, (err, results) => {
    res.json(results);
  });
});

Good:

javascript
// 解决方案:使用参数化查询
function getUser(username, password) {
  // 使用参数化查询防止SQL注入
  const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
  return db.query(query, [username, password]);
}

// 或使用ORM/Query Builder
const user = await User.where({
  username: username,
  password: password
}).fetch();

// 对于GET请求,验证输入类型
app.get('/user/:id', (req, res) => {
  // 验证输入类型
  const userId = parseInt(req.params.id, 10);
  
  if (isNaN(userId)) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }
  
  // 使用参数化查询
  const query = 'SELECT * FROM users WHERE id = ?';
  db.query(query, [userId], (err, results) => {
    if (err) {
      console.error('Database error:', err);
      return res.status(500).json({ error: 'Internal server error' });
    }
    res.json(results);
  });
});

2. NoSQL注入

Bad:

javascript
// 问题:直接使用用户输入构建查询
app.get('/api/users', (req, res) => {
  const query = req.query;
  // 直接将用户输入用于MongoDB查询
  User.find(query).then(users => {
    res.json(users);
  });
});

Good:

javascript
// 解决方案:验证和净化查询参数
app.get('/api/users', (req, res) => {
  const { name, email } = req.query;
  
  // 只允许安全的查询字段
  const safeQuery = {};
  
  if (name && typeof name === 'string' && name.length <= 50) {
    safeQuery.name = new RegExp(name, 'i');
  }
  
  if (email && typeof email === 'string' && email.length <= 100) {
    safeQuery.email = email;
  }
  
  User.find(safeQuery).then(users => {
    res.json(users);
  }).catch(err => {
    console.error('Query error:', err);
    res.status(500).json({ error: 'Internal server error' });
  });
});

3. OS命令注入

Bad:

javascript
// 问题:直接将用户输入用于系统命令
app.post('/api/execute', (req, res) => {
  const command = req.body.command;
  // 危险:直接执行用户输入的命令
  const result = exec(command);
  res.json({ output: result.stdout });
});

Good:

javascript
// 解决方案:限制可执行的命令或使用白名单
const ALLOWED_COMMANDS = ['ls', 'pwd', 'whoami'];

app.post('/api/execute', (req, res) => {
  const { command, args } = req.body;
  
  // 验证命令是否在白名单中
  if (!ALLOWED_COMMANDS.includes(command)) {
    return res.status(400).json({ error: 'Command not allowed' });
  }
  
  // 使用安全的参数传递
  const spawn = require('child_process').spawn;
  const child = spawn(command, args || []);
  
  let output = '';
  child.stdout.on('data', (data) => {
    output += data.toString();
  });
  
  child.on('close', (code) => {
    res.json({ output, exitCode: code });
  });
});

XSS(跨站脚本攻击)防护

1. 反射型XSS

Bad:

javascript
// 问题:直接将URL参数输出到页面
app.get('/search', (req, res) => {
  const searchTerm = req.query.q || '';
  // 危险:直接输出用户输入到HTML
  res.send(`
    <html>
      <body>
        <h1>搜索结果:${searchTerm}</h1>
        <!-- 其他内容 -->
      </body>
    </html>
  `);
});

// 前端问题代码
function displaySearchResult(term) {
  // 危险:直接插入HTML
  document.getElementById('search-result').innerHTML = term;
}

Good:

javascript
// 解决方案:使用模板引擎或手动转义
const escapeHtml = require('escape-html');

app.get('/search', (req, res) => {
  const searchTerm = req.query.q || '';
  
  // 转义HTML特殊字符
  const escapedTerm = escapeHtml(searchTerm);
  
  res.render('search-results', { 
    searchTerm: escapedTerm,
    results: performSearch(searchTerm) 
  });
});

// 前端安全代码
function displaySearchResult(term) {
  // 使用textContent而不是innerHTML
  document.getElementById('search-result').textContent = term;
  
  // 或者使用转义函数
  // document.getElementById('search-result').innerHTML = escapeHtml(term);
}

// 使用安全的模板引擎(如Handlebars)
// {{searchTerm}} 会自动转义特殊字符

2. 存储型XSS

Bad:

javascript
// 问题:保存用户输入时未进行净化
app.post('/api/comments', (req, res) => {
  const { content, userId } = req.body;
  
  // 直接保存,未净化内容
  const comment = new Comment({
    content: content,  // 未净化
    userId: userId
  });
  
  comment.save().then(saved => {
    res.json(saved);
  });
});

Good:

javascript
// 解决方案:保存前净化内容
const sanitizeHtml = require('sanitize-html');

app.post('/api/comments', (req, res) => {
  const { content, userId } = req.body;
  
  // 净化HTML内容,只允许安全的标签
  const sanitizedContent = sanitizeHtml(content, {
    allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br'],
    allowedAttributes: {}
  });
  
  const comment = new Comment({
    content: sanitizedContent,
    userId: userId
  });
  
  comment.save().then(saved => {
    res.json(saved);
  }).catch(err => {
    console.error('Save error:', err);
    res.status(500).json({ error: 'Failed to save comment' });
  });
});

3. DOM-based XSS

Bad:

javascript
// 问题:直接使用URL参数修改DOM
function initPage() {
  const urlParams = new URLSearchParams(window.location.search);
  const redirect = urlParams.get('redirect');
  
  if (redirect) {
    // 危险:可能导致DOM-based XSS
    window.location.href = redirect;
  }
}

// 另一个例子
function setLanguage() {
  const lang = localStorage.getItem('lang') || 'en';
  // 危险:如果lang包含恶意脚本
  document.write(`<script src="/locales/${lang}.js"></script>`);
}

Good:

javascript
// 解决方案:验证和限制重定向
function initPage() {
  const urlParams = new URLSearchParams(window.location.search);
  const redirect = urlParams.get('redirect');
  
  if (redirect) {
    // 验证重定向URL的安全性
    if (isValidRedirectUrl(redirect)) {
      window.location.href = redirect;
    } else {
      console.warn('Invalid redirect URL:', redirect);
      // 使用默认页面
      window.location.href = '/';
    }
  }
}

function isValidRedirectUrl(url) {
  try {
    const parsedUrl = new URL(url);
    // 只允许同域重定向
    return parsedUrl.origin === window.location.origin;
  } catch (e) {
    return false;
  }
}

// 安全的语言设置
function setLanguage() {
  const validLangs = ['en', 'zh', 'ja', 'ko']; // 白名单
  const lang = localStorage.getItem('lang') || 'en';
  
  if (validLangs.includes(lang)) {
    const script = document.createElement('script');
    script.src = `/locales/${lang}.js`;
    script.type = 'text/javascript';
    document.head.appendChild(script);
  } else {
    console.warn('Invalid language:', lang);
  }
}

认证和授权安全

1. 密码安全

Bad:

javascript
// 问题:明文存储密码
app.post('/api/register', (req, res) => {
  const { username, password, email } = req.body;
  
  const user = new User({
    username: username,
    password: password,  // 明文存储密码!
    email: email
  });
  
  user.save().then(() => {
    res.json({ success: true });
  });
});

Good:

javascript
// 解决方案:安全的密码处理
const bcrypt = require('bcryptjs');

app.post('/api/register', async (req, res) => {
  const { username, password, email } = req.body;
  
  try {
    // 验证密码强度
    if (!validatePasswordStrength(password)) {
      return res.status(400).json({ error: 'Password does not meet requirements' });
    }
    
    // 加密密码
    const saltRounds = 12;
    const hashedPassword = await bcrypt.hash(password, saltRounds);
    
    const user = new User({
      username: username,
      password: hashedPassword,  // 存储加密后的密码
      email: email
    });
    
    await user.save();
    res.json({ success: true });
  } catch (error) {
    console.error('Registration error:', error);
    res.status(500).json({ error: 'Registration failed' });
  }
});

// 密码验证
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    const user = await User.findOne({ username });
    
    if (!user) {
      // 注意:不要区分用户名不存在和密码错误
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // 安全比较密码
    const isMatch = await bcrypt.compare(password, user.password);
    
    if (!isMatch) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // 生成安全的token
    const token = generateSecureToken(user._id);
    res.json({ token, userId: user._id });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Login failed' });
  }
});

function validatePasswordStrength(password) {
  // 至少8位,包含大小写字母、数字和特殊字符
  const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return strongPasswordRegex.test(password);
}

2. JWT安全

Bad:

javascript
// 问题:不安全的JWT实现
const jwt = require('jsonwebtoken');

// 使用弱密钥
const SECRET = 'password';  // 太简单!

function generateToken(user) {
  // 没有过期时间,没有其他安全选项
  return jwt.sign({ userId: user.id }, SECRET);
}

// 不验证token
app.use('/api/protected', (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, SECRET);  // 同步验证,可能被绕过
  req.user = decoded;
  next();
});

Good:

javascript
// 解决方案:安全的JWT实现
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// 使用强密钥
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(64).toString('hex');

function generateToken(user) {
  return jwt.sign(
    { 
      userId: user.id,
      roles: user.roles,
      exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24) // 24小时过期
    },
    JWT_SECRET,
    { 
      algorithm: 'HS256',
      issuer: 'myapp.com',
      audience: 'myapp-users'
    }
  );
}

// 安全的token验证中间件
function authenticateToken(req, res, next) {
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(' ')[1];  // Bearer TOKEN

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

  jwt.verify(token, JWT_SECRET, (err, decoded) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Token expired' });
      }
      return res.status(403).json({ error: 'Invalid token' });
    }
    
    req.user = decoded;
    next();
  });
}

// 使用中间件
app.get('/api/protected/data', authenticateToken, (req, res) => {
  res.json({ data: 'protected content', user: req.user });
});

3. 权限控制

Bad:

javascript
// 问题:权限控制不充分
app.delete('/api/users/:id', authenticateToken, async (req, res) => {
  const userId = req.params.id;
  
  // 任何人都可以删除任何用户!
  await User.findByIdAndDelete(userId);
  res.json({ success: true });
});

Good:

javascript
// 解决方案:适当的权限控制
function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    if (req.user.permissions && req.user.permissions.includes(permission)) {
      next();
    } else {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
  };
}

// 基于角色的访问控制
function requireRole(role) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    
    if (req.user.role === role || req.user.role === 'admin') {
      next();
    } else {
      return res.status(403).json({ error: 'Insufficient privileges' });
    }
  };
}

// 检查资源所有权
function checkOwnership(model, ownerIdField = 'userId') {
  return async (req, res, next) => {
    const resourceId = req.params.id;
    const resource = await model.findById(resourceId);
    
    if (!resource) {
      return res.status(404).json({ error: 'Resource not found' });
    }
    
    if (resource[ownerIdField].toString() !== req.user.userId.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ error: 'Access denied' });
    }
    
    next();
  };
}

// 应用权限控制
app.delete('/api/users/:id', authenticateToken, requireRole('admin'), async (req, res) => {
  const userId = req.params.id;
  
  // 只有管理员才能删除用户
  await User.findByIdAndDelete(userId);
  res.json({ success: true });
});

app.delete('/api/profile/:id', authenticateToken, checkOwnership(User, '_id'), async (req, res) => {
  const userId = req.params.id;
  
  // 用户只能删除自己的资料
  if (req.user.userId.toString() !== userId) {
    return res.status(403).json({ error: 'Cannot delete other users\' profiles' });
  }
  
  await User.findByIdAndDelete(userId);
  res.json({ success: true });
});

数据保护和隐私

1. 敏感数据处理

Bad:

javascript
// 问题:记录敏感数据到日志
app.post('/api/payment', async (req, res) => {
  const { cardNumber, cvv, expiryDate, amount } = req.body;
  
  // 记录完整的支付信息到日志 - 非常危险!
  console.log('Payment request:', { cardNumber, cvv, expiryDate, amount });
  
  const result = await processPayment(cardNumber, cvv, expiryDate, amount);
  res.json(result);
});

Good:

javascript
// 解决方案:安全处理敏感数据
app.post('/api/payment', async (req, res) => {
  const { cardNumber, cvv, expiryDate, amount } = req.body;
  
  // 只记录必要的、非敏感的信息
  console.log('Payment request:', {
    maskedCard: maskCardNumber(cardNumber),
    amount: amount,
    timestamp: new Date().toISOString()
  });
  
  try {
    const result = await processPayment(cardNumber, cvv, expiryDate, amount);
    res.json(result);
  } catch (error) {
    // 记录错误,但不包含敏感信息
    console.error('Payment processing failed:', {
      amount: amount,
      userId: req.user?.id,
      timestamp: new Date().toISOString()
    });
    
    res.status(500).json({ error: 'Payment processing failed' });
  }
});

function maskCardNumber(cardNumber) {
  if (!cardNumber) return '';
  const str = cardNumber.toString();
  return str.substring(0, 6) + '****' + str.substring(str.length - 4);
}

2. 数据加密

Bad:

javascript
// 问题:明文存储敏感数据
const user = new User({
  name: 'John Doe',
  ssn: '123-45-6789',  // 社保号明文存储
  creditCard: '4111111111111111'  // 信用卡号明文存储
});

Good:

javascript
// 解决方案:使用加密存储敏感数据
const crypto = require('crypto');
const algorithm = 'aes-256-gcm';

class SecureDataStorage {
  constructor(key) {
    this.key = Buffer.from(key, 'hex');
  }

  encrypt(text) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher(algorithm, this.key);
    cipher.setAAD(Buffer.from('additional-data'));
    
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    const authTag = cipher.getAuthTag();
    
    return {
      encrypted: encrypted,
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex')
    };
  }

  decrypt(encryptedData) {
    const { encrypted, iv, authTag } = encryptedData;
    
    const decipher = crypto.createDecipher(algorithm, this.key);
    decipher.setAAD(Buffer.from('additional-data'));
    decipher.setAuthTag(Buffer.from(authTag, 'hex'));
    
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    
    return decrypted;
  }
}

// 使用示例
const secureStorage = new SecureDataStorage(process.env.DATA_ENCRYPTION_KEY);

const user = new User({
  name: 'John Doe',
  encryptedData: {
    ssn: secureStorage.encrypt('123-45-6789'),
    creditCard: secureStorage.encrypt('4111111111111111')
  }
});

// 访问时解密
app.get('/api/user/:id', authenticateToken, async (req, res) => {
  const user = await User.findById(req.params.id);
  
  // 只有授权用户才能访问敏感数据
  if (req.user.id !== user._id && req.user.role !== 'admin') {
    // 返回不包含敏感数据的用户信息
    const { encryptedData, ...publicUserInfo } = user.toObject();
    res.json(publicUserInfo);
  } else {
    // 授权用户可以访问解密后的数据
    const decryptedData = {
      ssn: secureStorage.decrypt(user.encryptedData.ssn),
      creditCard: secureStorage.decrypt(user.encryptedData.creditCard)
    };
    
    res.json({
      ...user.toObject(),
      decryptedData
    });
  }
});

安全配置和实践

1. HTTP安全头

Bad:

javascript
// 问题:缺少安全头
app.get('/', (req, res) => {
  res.send('<h1>Hello World</h1>');
});

Good:

javascript
// 解决方案:设置安全头
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'"],
      upgradeInsecureRequests: true,
    },
  },
  hsts: {
    maxAge: 31536000, // 1年
    includeSubDomains: true,
    preload: true
  },
  frameguard: {
    action: 'deny'
  },
  noSniff: true,
}));

// 或者手动设置安全头
app.use((req, res, next) => {
  // 防止MIME类型混淆攻击
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // 防止点击劫持
  res.setHeader('X-Frame-Options', 'DENY');
  
  // 防止XSS
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // 强制HTTPS
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  
  next();
});

2. 错误处理安全

Bad:

javascript
// 问题:泄露内部信息
app.get('/api/data/:id', async (req, res) => {
  try {
    const data = await getDataById(req.params.id);
    res.json(data);
  } catch (error) {
    // 危险:返回详细错误信息
    res.status(500).json({ error: error.message, stack: error.stack });
  }
});

Good:

javascript
// 解决方案:安全的错误处理
app.get('/api/data/:id', async (req, res) => {
  try {
    const data = await getDataById(req.params.id);
    res.json(data);
  } catch (error) {
    // 记录详细错误到服务器日志
    console.error('Data retrieval error:', {
      error: error.message,
      stack: error.stack,
      params: req.params,
      userId: req.user?.id
    });
    
    // 返回通用错误信息给客户端
    res.status(500).json({ error: 'An error occurred while retrieving data' });
  }
});

// 全局错误处理中间件
app.use((error, req, res, next) => {
  // 记录错误
  console.error('Unhandled error:', error);
  
  // 根据错误类型返回适当的响应
  if (error.name === 'ValidationError') {
    return res.status(400).json({ error: 'Validation failed' });
  }
  
  if (error.name === 'UnauthorizedError') {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  // 对于所有其他错误,返回通用错误信息
  res.status(500).json({ error: 'Internal server error' });
});

安全审查最佳实践

1. 安全审查清单

在代码审查中,使用以下清单检查安全问题:

输入验证:

  • [ ] 所有外部输入都经过验证
  • [ ] 验证了输入长度、类型和格式
  • [ ] 对特殊字符进行了净化
  • [ ] 实现了适当的参数化查询

输出编码:

  • [ ] 对HTML输出进行了转义
  • [ ] 对JavaScript、CSS、URL上下文进行了适当编码
  • [ ] 使用了安全的模板引擎

认证和授权:

  • [ ] 实现了安全的密码处理
  • [ ] 使用了安全的会话管理
  • [ ] 实现了适当的权限控制
  • [ ] 保护了敏感操作

数据保护:

  • [ ] 敏感数据进行了加密
  • [ ] 避免了在日志中记录敏感信息
  • [ ] 实现了数据访问控制

配置安全:

  • [ ] 设置了适当的安全头
  • [ ] 实现了安全的错误处理
  • [ ] 使用了安全的依赖包

2. 安全工具集成

javascript
// package.json - 安全相关的依赖
{
  "devDependencies": {
    "eslint-plugin-security": "^1.4.0",  // 检测安全问题
    "retire": "^2.2.0",                  // 检测易受攻击的依赖
    "snyk": "^1.1000.0"                 // 漏洞检测
  },
  "scripts": {
    "security:audit": "npm audit --audit-level moderate",
    "security:check": "snyk test",
    "security:lint": "eslint --plugin security --ext .js,.ts src/"
  }
}

// ESLint安全规则配置
// .eslintrc.js
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    'security/detect-object-injection': 'warn',
    'security/detect-non-literal-regexp': 'error',
    'security/detect-unsafe-regex': 'error'
  }
};

通过在代码审查中关注这些安全问题,可以及早发现和修复安全漏洞,确保应用程序的安全性。