Appearance
代码审查中的安全考虑
安全是软件开发中最重要的考虑因素之一。在代码审查过程中,必须仔细检查代码的安全性,以防止各种安全漏洞。本章将详细介绍在代码审查中如何识别和防范安全问题。
安全审查清单
在代码审查中,应重点关注以下安全相关问题:
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'
}
};
通过在代码审查中关注这些安全问题,可以及早发现和修复安全漏洞,确保应用程序的安全性。