SQL注入防护与代码规范

工具相关 ·

SQL注入是最古老也最危险的Web安全漏洞之一。攻击者通过在应用程序的输入中插入恶意的SQL代码片段,改变原有SQL语句的逻辑,从而窃取数据、篡改数据,甚至获取服务器的控制权。本文全面介绍SQL注入的原理、危害、防护方法以及安全编码规范。

一、SQL注入原理

1.1 什么是SQL注入

SQL注入的本质是:应用程序将用户输入的数据直接拼接到SQL语句中,而没有对输入进行任何过滤或转义。

1.2 注入示例

存在注入漏洞的登录代码:

# 危险代码示例(Python + MySQL)
def login(username, password):
    # 直接拼接用户输入到SQL语句中
    sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    cursor.execute(sql)
    return cursor.fetchone()

攻击方式:

用户名输入:admin' --
密码输入:任意值

实际执行的SQL:
SELECT * FROM users WHERE username = 'admin' --' AND password = '任意值'

-- 后面的内容被注释掉了,等价于:
SELECT * FROM users WHERE username = 'admin'

1.3 更危险的注入

用户名输入:' OR '1'='1' --

实际执行的SQL:
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = '...'

-- 条件 '1'='1' 永远为真,返回所有用户数据

1.4 常见注入类型

注入类型描述危害等级
联合查询注入使用UNION SELECT合并数据高
报错注入利用数据库报错信息泄露数据高
布尔盲注通过页面返回真假来判断条件中高
时间盲注通过响应延迟来判断条件中
堆叠注入执行多条SQL语句极高
二次注入存储后再取出时触发高

二、防护方法

2.1 参数化查询(最核心的防护手段)

使用预处理语句:

# Python示例 - 安全的参数化查询
import mysql.connector

def get_user_safe(username):
    conn = mysql.connector.connect(host='localhost', user='app', password='xxx')
    cursor = conn.cursor()

    # 安全:使用参数化查询,%s是占位符
    sql = "SELECT user_id, username, email FROM users WHERE username = %s"
    cursor.execute(sql, (username,))
    return cursor.fetchone()
// Java示例 - PreparedStatement
public User getUser(String username) throws SQLException {
    String sql = "SELECT user_id, username, email FROM users WHERE username = ?";

    try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
        pstmt.setString(1, username);  // 参数自动转义
        ResultSet rs = pstmt.executeQuery();
        if (rs.next()) {
            User user = new User();
            user.setId(rs.getInt("user_id"));
            user.setName(rs.getString("username"));
            user.setEmail(rs.getString("email"));
            return user;
        }
    }
    return null;
}
// Node.js示例 - 参数化查询
const getUser = async (username) => {
    const sql = 'SELECT user_id, username, email FROM users WHERE username = ?';
    const [rows] = await pool.execute(sql, [username]);
    return rows[0];
};
// PHP示例 - PDO参数化
function getUser($username) {
    $stmt = $pdo->prepare('SELECT user_id, username, email FROM users WHERE username = :username');
    $stmt->execute(['username' => $username]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

2.2 存储过程防护

-- 使用存储过程封装查询逻辑
CREATE PROCEDURE sp_get_user_by_name(
    IN p_username VARCHAR(50)
)
BEGIN
    SELECT user_id, username, email, created_at
    FROM users
    WHERE username = p_username
    LIMIT 1;
END;

-- 调用时应用程序只传递参数
CALL sp_get_user_by_name('zhangsan');

2.3 输入验证

# 输入验证示例
import re

def validate_user_input(user_input, input_type):
    """验证用户输入是否合法"""
    validators = {
        'username': {
            'pattern': r'^[a-zA-Z0-9_]{3,20}$',
            'message': '用户名只能包含字母、数字和下划线,长度3-20'
        },
        'email': {
            'pattern': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
            'message': '邮箱格式不正确'
        },
        'integer': {
            'pattern': r'^-?\d+$',
            'message': '请输入有效的整数'
        },
        'date': {
            'pattern': r'^\d{4}-\d{2}-\d{2}$',
            'message': '日期格式必须为YYYY-MM-DD'
        }
    }

    if input_type not in validators:
        return False, '未知的输入类型'

    if not re.match(validators[input_type]['pattern'], str(user_input)):
        return False, validators[input_type]['message']

    return True, '验证通过'

2.4 白名单验证

# 对于ORDER BY等无法参数化的场景,使用白名单
def get_users(sort_by='created_at', sort_order='DESC'):
    # 白名单验证排序字段
    allowed_sort_fields = {'created_at', 'username', 'email', 'login_count'}
    allowed_sort_orders = {'ASC', 'DESC'}

    if sort_by not in allowed_sort_fields:
        sort_by = 'created_at'

    if sort_order.upper() not in allowed_sort_orders:
        sort_order = 'DESC'

    # 安全:排序字段来自白名单,不是用户输入
    sql = f"SELECT user_id, username, email FROM users ORDER BY {sort_by} {sort_order} LIMIT 50"
    cursor.execute(sql)
    return cursor.fetchall()

三、ORM安全实践

3.1 ORM并非万能

ORM(对象关系映射)框架通常能自动处理参数化,但不当使用仍可能引入注入风险:

# Django ORM - 安全的常规用法
User.objects.filter(username=username)  # 安全:自动参数化

# Django ORM - 危险的raw查询
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")  # 危险!

# Django ORM - 安全的raw查询
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])  # 安全
// JPA/Hibernate - 安全用法
@Query("SELECT u FROM User u WHERE u.username = :username")
List<User> findByUsername(@Param("username") String username);

// JPA/Hibernate - 危险用法(字符串拼接JPQL)
String jpql = "SELECT u FROM User u WHERE u.username = '" + username + "'";
entityManager.createQuery(jpql);  // 危险!

3.2 ORM安全使用规范

做法安全性说明
ORM标准查询方法安全框架自动参数化
参数化raw/query安全正确使用占位符
字符串拼接raw SQL危险直接拼接用户输入
动态表名/列名需验证必须使用白名单
原生SQL传递需验证始终使用参数绑定

四、安全编码规范

4.1 数据库账号权限分离

-- 应用程序账号:只授予必要的权限
CREATE USER 'app_user'@'%' IDENTIFIED BY 'StrongPassword123!';

-- 只读权限(用于查询操作)
GRANT SELECT ON mydb.products TO 'app_user'@'%';
GRANT SELECT ON mydb.categories TO 'app_user'@'%';

-- 读写权限(用于必要的写入)
GRANT SELECT, INSERT, UPDATE ON mydb.orders TO 'app_user'@'%';
GRANT SELECT, INSERT ON mydb.order_items TO 'app_user'@'%';

-- 绝不授予的权限
-- 不授予 DROP, CREATE, ALTER(防止表结构被修改)
-- 不授予 GRANT(防止权限扩散)
-- 不授予 FILE(防止读写服务器文件)
-- 不授予 SUPER, PROCESS(防止系统级操作)

4.2 安全编码规范清单

SQL安全编码规范 v1.0
=====================

1. 所有用户输入必须通过参数化查询传递
2. 禁止使用字符串拼接构建SQL语句
3. 动态表名、列名必须使用白名单验证
4. 数据库账号遵循最小权限原则
5. 敏感数据必须加密存储
6. 数据库错误信息不得直接返回给用户
7. LIKE查询的通配符需要转义处理
8. 批量操作必须使用事务保护
9. 定期审查数据库账号权限
10. 所有数据库操作记录审计日志

4.3 错误处理规范

# 不通过:将数据库错误信息直接暴露给用户
def search_products(keyword):
    try:
        sql = f"SELECT * FROM products WHERE name LIKE '%{keyword}%'"
        return cursor.execute(sql).fetchall()
    except Exception as e:
        return {"error": str(e)}  # 暴露数据库结构信息
        # 攻击者可能通过错误信息推断表名、列名等

# 通过:捕获异常并返回通用错误信息
def search_products(keyword):
    try:
        sql = "SELECT product_id, name, price FROM products WHERE name LIKE %s"
        search_term = f"%{keyword}%"
        cursor.execute(sql, (search_term,))
        return cursor.fetchall()
    except mysql.connector.Error as e:
        # 记录详细错误到日志
        logger.error(f"Database error: {e}, query keyword: {keyword}")
        # 返回通用错误信息给用户
        return {"error": "搜索暂时不可用,请稍后重试"}

五、防护体系

5.1 多层防护架构

用户请求
    │
    ▼
┌─────────────────────────────┐
│  第一层:WAF(Web应用防火墙) │  ← 拦截明显的注入模式
└─────────────────────────────┘
    │
    ▼
┌─────────────────────────────┐
│  第二层:输入验证             │  ← 白名单验证、类型检查
└─────────────────────────────┘
    │
    ▼
┌─────────────────────────────┐
│  第三层:参数化查询           │  ← 数据库驱动层面转义
└─────────────────────────────┘
    │
    ▼
┌─────────────────────────────┐
│  第四层:最小权限数据库账号   │  ← 即使注入也限制损害范围
└─────────────────────────────┘
    │
    ▼
┌─────────────────────────────┐
│  第五层:审计日志与监控       │  ← 异常行为检测和告警
└─────────────────────────────┘

5.2 安全测试清单

测试项测试方法通过标准
登录注入输入 ' OR '1'='1登录失败,不返回异常数据
搜索注入输入 '; DROP TABLE users; --搜索正常执行,表不被删除
排序注入修改sort参数为SQL片段参数被忽略或使用默认值
分页注入修改page/limit为非数字使用默认值或返回错误
批量注入在批量操作字段中注入每个字段独立验证
二次注入注册含SQL的用户名用户名被正确转义存储

六、总结

SQL注入防护的核心原则可以总结为:

  1. 参数化查询是第一防线:所有用户输入必须通过参数化方式传递
  2. 输入验证是必要补充:白名单验证确保输入符合预期格式
  3. 最小权限限制损害:即使发生注入,也限制攻击者能做的事情
  4. ORM不是万能药:raw查询和动态SQL仍需特别注意
  5. 多层防护纵深防御:WAF、验证、参数化、权限、监控层层设防

记住:SQL注入是100%可以预防的漏洞。只要严格遵循安全编码规范,就能彻底消除SQL注入风险。

阅读 13