Appearance
SQL 注入防护
SQL 注入(SQL Injection)是一种代码注入攻击,攻击者通过在输入字段中插入恶意的 SQL 代码来操纵数据库查询,可能导致数据泄露、数据损坏甚至服务器入侵。
SQL 注入原理
基本概念
SQL 注入发生在应用程序未能正确验证用户输入的情况下。当用户输入被直接拼接到 SQL 查询中时,恶意用户可以构造特殊的输入来改变查询的逻辑。
攻击示例
易受攻击的代码:
javascript
// 危险的查询 - 不要这样做!
const userInput = req.body.username; // 用户输入: admin' --
const query = `SELECT * FROM users WHERE username = '${userInput}'`;
// 实际执行的 SQL:
// SELECT * FROM users WHERE username = 'admin' --'
// 这将注释掉查询的其余部分,返回所有用户
更复杂的注入示例:
javascript
// 用户输入: admin'; DROP TABLE users; --
// 生成的查询:
// SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
SQL 注入类型
1. 基于错误的注入
攻击者通过输入特殊字符引发数据库错误来获取数据库结构信息。
sql
-- 输入示例
' OR 1=1 --
-- 引发错误的输入
' AND (SELECT COUNT(*) FROM sysobjects) > 0 --
2. 基于布尔的注入
攻击者通过布尔条件判断数据库内容。
sql
-- 尝试获取用户名
' OR SUBSTRING(username, 1, 1) = 'a' --
3. 基于时间的注入
攻击者通过时间延迟来判断查询结果。
sql
-- MySQL 示例
' OR IF(1=1, SLEEP(5), 0) --
4. 联合查询注入
攻击者使用 UNION 操作符获取额外数据。
sql
-- 输入
' UNION SELECT username, password FROM admin_users --
防护措施
1. 参数化查询(推荐)
参数化查询是防止 SQL 注入最有效的方法。
javascript
// 使用参数化查询 - 安全
const { Pool } = require('pg');
const pool = new Pool({
// 数据库配置
});
// 安全的查询方式
app.get('/user/:id', async (req, res) => {
const userId = req.params.id;
// 使用参数化查询
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[userId] // 参数数组
);
res.json(result.rows);
});
// 登录示例
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const result = await pool.query(
'SELECT * FROM users WHERE username = $1 AND password = $2',
[username, password] // 参数会被正确转义
);
if (result.rows.length > 0) {
res.json({ success: true });
} else {
res.json({ success: false });
}
});
2. 使用 ORM 和查询构建器
javascript
const { Sequelize, DataTypes } = require('sequelize');
// 使用 Sequelize ORM
const sequelize = new Sequelize('database', 'username', 'password', {
dialect: 'postgres'
});
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
});
// 安全的查询 - ORM 会自动参数化
app.get('/user/:id', async (req, res) => {
const userId = req.params.id;
try {
const user = await User.findByPk(userId);
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Database error' });
}
});
// 使用查询构建器
app.get('/search-users', async (req, res) => {
const { searchTerm } = req.query;
try {
// 查询构建器会自动参数化
const users = await User.findAll({
where: {
[Sequelize.Op.or]: [
{ username: { [Sequelize.Op.like]: `%${searchTerm}%` } },
{ email: { [Sequelize.Op.like]: `%${searchTerm}%` } }
]
}
});
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Database error' });
}
});
3. 输入验证和过滤
javascript
// 输入验证中间件
function validateInput(req, res, next) {
// 验证 ID 格式(纯数字)
if (req.params.id && !/^\d+$/.test(req.params.id)) {
return res.status(400).json({ error: 'Invalid ID format' });
}
// 验证用户名格式
if (req.body.username && !/^[a-zA-Z0-9_]{3,20}$/.test(req.body.username)) {
return res.status(400).json({ error: 'Invalid username format' });
}
// 验证邮箱格式
if (req.body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(req.body.email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
next();
}
// 使用验证中间件
app.get('/user/:id', validateInput, getUserHandler);
app.post('/register', validateInput, registerUserHandler);
4. 使用白名单验证
javascript
// 对于有限的选择项,使用白名单
const allowedSortFields = ['name', 'email', 'created_at'];
const allowedOrderDirections = ['ASC', 'DESC'];
app.get('/users', (req, res) => {
let { sort, order } = req.query;
// 验证排序字段
if (sort && !allowedSortFields.includes(sort)) {
return res.status(400).json({ error: 'Invalid sort field' });
}
// 验证排序方向
if (order && !allowedOrderDirections.includes(order.toUpperCase())) {
return res.status(400).json({ error: 'Invalid order direction' });
}
// 使用验证后的参数构建查询
const sortOrder = order ? order.toUpperCase() : 'ASC';
// 安全的查询构建
const query = `SELECT * FROM users ORDER BY ${sort || 'name'} ${sortOrder}`;
// 注意:在这个例子中,我们已经验证了 sort 和 sortOrder,
// 但在生产环境中,最好使用参数化查询或 ORM
});
框架特定的防护
Express.js with MySQL
javascript
const mysql = require('mysql2/promise');
// 创建连接池
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 安全的查询示例
app.get('/product/:id', async (req, res) => {
const productId = req.params.id;
try {
// 使用参数化查询
const [rows] = await pool.execute(
'SELECT * FROM products WHERE id = ?',
[productId]
);
res.json(rows);
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: 'Database error' });
}
});
// 搜索查询
app.get('/search-products', async (req, res) => {
const { q, minPrice, maxPrice } = req.query;
let query = 'SELECT * FROM products WHERE 1=1';
const params = [];
if (q) {
query += ' AND name LIKE ?';
params.push(`%${q}%`);
}
if (minPrice) {
query += ' AND price >= ?';
params.push(parseFloat(minPrice));
}
if (maxPrice) {
query += ' AND price <= ?';
params.push(parseFloat(maxPrice));
}
try {
const [rows] = await pool.execute(query, params);
res.json(rows);
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
MongoDB 查询注入防护
虽然 MongoDB 不是 SQL 数据库,但仍可能受到类似注入的攻击:
javascript
const { MongoClient } = require('mongodb');
// 安全的 MongoDB 查询
app.get('/user/:id', async (req, res) => {
const userId = req.params.id;
try {
// 使用 ObjectId 转换来确保类型安全
const { ObjectId } = require('mongodb');
if (!ObjectId.isValid(userId)) {
return res.status(400).json({ error: 'Invalid user ID' });
}
const user = await db.collection('users').findOne({
_id: new ObjectId(userId)
});
res.json(user);
} catch (error) {
console.error('Database error:', error);
res.status(500).json({ error: 'Database error' });
}
});
// 搜索查询 - 避免直接使用用户输入
app.get('/search-users', async (req, res) => {
const { name } = req.query;
// 验证和清理输入
if (typeof name !== 'string' || name.length > 50) {
return res.status(400).json({ error: 'Invalid search term' });
}
// 使用正则表达式进行模糊搜索
const users = await db.collection('users').find({
name: { $regex: new RegExp(name, 'i') } // 不区分大小写
}).toArray();
res.json(users);
});
检测 SQL 注入
日志监控
javascript
// 查询日志中间件
function queryLogger(req, res, next) {
const startTime = Date.now();
const originalSend = res.send;
res.send = function(data) {
const duration = Date.now() - startTime;
// 记录可疑的查询模式
const suspiciousPatterns = [
/(\bUNION\b|\bDROP\b|\bDELETE\b|\bUPDATE\b|\bINSERT\b|\bEXEC\b)/i,
/('|--|\/\*|\*\/|;)/,
/(information_schema|sysobjects|pg_tables)/i
];
const queryString = JSON.stringify(req.query);
const requestBody = JSON.stringify(req.body);
for (const pattern of suspiciousPatterns) {
if (pattern.test(queryString) || pattern.test(requestBody)) {
console.warn('Suspicious query detected:', {
url: req.url,
method: req.method,
query: req.query,
body: req.body,
timestamp: new Date().toISOString()
});
break;
}
}
// 记录慢查询
if (duration > 1000) { // 超过1秒
console.info('Slow query detected:', {
url: req.url,
duration: duration,
method: req.method
});
}
return originalSend.call(this, data);
};
next();
}
app.use(queryLogger);
WAF (Web Application Firewall)
javascript
// 简单的输入过滤中间件
function wafMiddleware(req, res, next) {
// 检查请求中的恶意模式
const maliciousPatterns = [
/(\bUNION\b|\bSELECT\b|\bINSERT\b|\bUPDATE\b|\bDELETE\b|\bDROP\b|\bCREATE\b|\bALTER\b)/i,
/('|--|;|\/\*|\*\/|xp_|sp_|exec|execute)/i,
/(information_schema|sysobjects|pg_tables|sqlite_master)/i
];
function checkForMalicious(content) {
if (typeof content !== 'string') return false;
for (const pattern of maliciousPatterns) {
if (pattern.test(content)) {
return true;
}
}
return false;
}
// 检查查询参数
for (const [key, value] of Object.entries(req.query)) {
if (checkForMalicious(value)) {
console.log('Blocked malicious query parameter:', { key, value });
return res.status(400).json({ error: 'Malicious input detected' });
}
}
// 检查请求体
for (const [key, value] of Object.entries(req.body)) {
if (checkForMalicious(value)) {
console.log('Blocked malicious body parameter:', { key, value });
return res.status(400).json({ error: 'Malicious input detected' });
}
}
next();
}
app.use(wafMiddleware);
修复已知的 SQL 注入漏洞
修复示例 1:动态查询
javascript
// 修复前 - 易受攻击
app.get('/filter-users', (req, res) => {
const { field, value } = req.query;
// 直接拼接,非常危险
const query = `SELECT * FROM users WHERE ${field} = '${value}'`;
// ...
});
// 修复后 - 使用白名单和参数化
app.get('/filter-users', (req, res) => {
const { field, value } = req.query;
// 白名单验证字段名
const allowedFields = ['name', 'email', 'status'];
if (!allowedFields.includes(field)) {
return res.status(400).json({ error: 'Invalid field' });
}
// 使用参数化查询
const query = `SELECT * FROM users WHERE ?? = ?`;
// ?? 用于转义字段名,? 用于转义值
connection.query(query, [field, value], (err, results) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(results);
});
});
修复示例 2:排序注入
javascript
// 修复前
app.get('/users', (req, res) => {
const { sortBy, order } = req.query;
const query = `SELECT * FROM users ORDER BY ${sortBy} ${order}`;
// ...
});
// 修复后
app.get('/users', (req, res) => {
let { sortBy, order } = req.query;
// 白名单验证
const allowedFields = ['name', 'email', 'created_at'];
const allowedOrders = ['ASC', 'DESC'];
sortBy = allowedFields.includes(sortBy) ? sortBy : 'name';
order = allowedOrders.includes(order?.toUpperCase()) ? order.toUpperCase() : 'ASC';
const query = 'SELECT * FROM users ORDER BY ?? ??';
connection.query(query, [sortBy, order], (err, results) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(results);
});
});
最佳实践总结
1. 永远不要信任用户输入
javascript
// 错误做法
const query = `SELECT * FROM users WHERE id = ${userId}`;
// 正确做法
const query = 'SELECT * FROM users WHERE id = ?';
connection.query(query, [userId]);
2. 使用参数化查询或预编译语句
javascript
// 最佳实践:总是使用参数化查询
const statement = connection.prepare('SELECT * FROM users WHERE id = ? AND status = ?');
const results = statement.execute([userId, status]);
3. 最小权限原则
sql
-- 为应用程序创建专用数据库用户
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- 只授予必要的权限
GRANT SELECT, INSERT, UPDATE ON app_db.users TO 'app_user'@'localhost';
GRANT SELECT, INSERT, UPDATE ON app_db.products TO 'app_user'@'localhost';
-- 不要授予 DROP、ALTER 等权限
4. 定期安全审计
javascript
// 定期扫描代码中的 SQL 注入漏洞
const fs = require('fs');
const path = require('path');
function scanForSqlInjection(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
scanForSqlInjection(filePath);
} else if (file.endsWith('.js')) {
const content = fs.readFileSync(filePath, 'utf8');
// 搜索可疑模式
const suspicious = [
/query\([^)]*\+[^)]*\)/, // 字符串拼接
/execute\([^)]*\+[^)]*\)/,
/(?:SELECT|INSERT|UPDATE|DELETE|CREATE|DROP).*\+/, // SQL + 拼接
];
for (const pattern of suspicious) {
if (pattern.test(content)) {
console.warn(`Potential SQL injection in ${filePath}:`);
console.warn(content.match(pattern)[0]);
}
}
}
}
}
总结
SQL 注入是最危险的 Web 应用漏洞之一,但通过以下措施可以有效防护:
- 始终使用参数化查询或预编译语句
- 实施严格的输入验证
- 使用 ORM 和查询构建器
- 应用最小权限原则
- 定期进行安全审计
- 监控和记录可疑活动
- 保持数据库和应用框架更新