Skip to content
On this page

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 应用漏洞之一,但通过以下措施可以有效防护:

  1. 始终使用参数化查询或预编译语句
  2. 实施严格的输入验证
  3. 使用 ORM 和查询构建器
  4. 应用最小权限原则
  5. 定期进行安全审计
  6. 监控和记录可疑活动
  7. 保持数据库和应用框架更新