乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

SQL 注入防范

导学

SQL 注入(SQL Injection)是 Web 安全领域中危害最大、最常见的攻击方式之一。它让攻击者能在你的 SQL 语句中插入恶意代码,从而窃取、篡改或删除数据。本节从原理到防御,彻底讲透 SQL 注入。

定义

SQL 注入:攻击者通过在用户输入中插入恶意 SQL 片段,使应用程序执行非预期的数据库操作。其本质是用户输入被当作代码执行。

建立测试环境

为了完整演示 SQL 注入的危害和防范方法,首先建立统一的测试数据库环境。

-- 创建测试数据库
CREATE DATABASE IF NOT EXISTS app_db CHARACTER SET utf8mb4;
USE app_db;

-- 创建员工表
CREATE TABLE employees (
    emp_id INT AUTO_INCREMENT PRIMARY KEY,
    emp_name VARCHAR(20),
    dept VARCHAR(20),
    score DECIMAL(5,2)
);

-- 创建分数表
CREATE TABLE scores (
    score_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_id INT,
    score DECIMAL(5,2)
);



-- 插入测试数据
INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);

INSERT INTO scores (emp_id, score) VALUES
(1, 90),
(2, 85);


当前数据状态(employees 表):

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部NULL

当前数据状态(scores 表):

score_idemp_idscore
1190
2285

经典注入案例

场景一:绕过登录验证

正常登录场景:

应用程序拼接的 SQL(假设用户输入 大翔 和 技术部):

SELECT * FROM employees WHERE emp_name = '大翔' AND dept = '技术部';

正常查询结果:

emp_idemp_namedeptscore
1大翔技术部100

结果:返回 1 条记录,查询成功,验证通过。

攻击者注入场景:

攻击者输入:

  • 用户名:' OR '1'='1
  • 部门:' OR '1'='1

应用程序拼接后的 SQL:

SELECT * FROM employees WHERE emp_name = '' OR '1'='1' AND dept = '' OR '1'='1';

注入攻击结果:

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部NULL

结果:返回 2 条记录(全表数据),攻击者无需知道任何真实数据即可绕过验证。

结果解读:

  • '1'='1' 是一个永远为真的条件,导致 WHERE 子句失去过滤作用。
  • AND 的优先级高于 OR,但攻击者构造的输入使逻辑变为 (emp_name='' OR '1'='1') AND (dept='' OR '1'='1'),由于 '1'='1' 为真,整个条件恒真。
  • 如果应用程序取返回结果的第一条记录作为登录用户,攻击者可能直接以 admin 身份登录。

场景二:数据窃取(UNION 注入)

正常查询场景:

应用程序拼接的 SQL(用户输入 2):

SELECT * FROM scores WHERE emp_id = 2;

正常查询结果:

score_idemp_idscore
2285

结果:返回 1 条记录,只看到当前员工的分数。

攻击者注入场景:

攻击者输入:2 UNION SELECT emp_id, emp_name, score FROM employees

应用程序拼接后的 SQL:

SELECT * FROM scores WHERE emp_id = 2
UNION
SELECT emp_id, emp_name, score FROM employees;

注入攻击结果:

score_idemp_idscore
2285
1大翔100
2白歌NULL

结果:攻击者不仅看到了当前员工的分数,还通过 UNION 注入窃取了 employees 表中的全部员工数据。

结果解读:

  • UNION 要求前后两个 SELECT 语句的列数相同。攻击者通过猜测或试错,使第二个查询的列数与第一个查询一致。
  • 两个 SELECT 语句的列数相同(都是 3 列),因此 UNION 可以成功执行。
  • 这种攻击不需要知道表结构,攻击者可以通过多次试探获取数据库的完整信息(表名、列名、数据)。

场景三:数据删除(毁灭性注入)

正常删除场景:

应用程序拼接的 SQL(用户输入 2):

DELETE FROM scores WHERE score_id = 2;

执行前数据:

score_idemp_idscore
1190
2285

执行后数据:

SELECT * FROM scores;
score_idemp_idscore
1190

结果:score_id = 2 的记录被正常删除。

攻击者注入场景:

攻击者输入:1; DROP TABLE scores; --

应用程序拼接后的 SQL:

DELETE FROM scores WHERE score_id = 1; DROP TABLE scores; -- ;

注入攻击结果:

SELECT * FROM scores;

结果:

ERROR 1146 (42S02): Table 'app_db.scores' doesn't exist

结果:

  • score_id = 1 的记录被删除(攻击者指定的第一个语句)。
  • scores 表被完全删除,数据永久丢失。
  • -- 是 MySQL 的注释符,其后的内容被忽略,避免了语法错误。

结果解读:

  • 分号 ; 在 MySQL 中用于分隔多条 SQL 语句。如果应用程序未限制单条语句执行,攻击者可以注入额外的破坏性命令。
  • 这种攻击可能导致数据表被删除、数据被篡改,甚至整个数据库被清空。
  • 防范的关键在于:永远不要让用户输入有机会被解析为 SQL 代码。

防范方法

方法一:参数化查询(PreparedStatement)—— 唯一正确的做法

原理:参数化查询将 SQL 语句和参数分开传递给数据库。数据库先编译 SQL 模板,再将参数作为纯数据绑定,参数永远不会被解析为 SQL 代码。

Java 示例(正确做法):

// 正确:使用 PreparedStatement,参数作为纯数据处理
String sql = "SELECT * FROM employees WHERE emp_name = ? AND dept = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "' OR '1'='1");  // 即使输入包含恶意字符,也只是普通字符串
pstmt.setString(2, "' OR '1'='1");
ResultSet rs = pstmt.executeQuery();

MySQL 层面验证(模拟参数化查询效果):

-- 使用 PREPARE 语句模拟参数化查询
PREPARE stmt FROM 'SELECT * FROM employees WHERE emp_name = ? AND dept = ?';
SET @user = ''' OR ''1''=''1';
SET @dept = ''' OR ''1''=''1';
EXECUTE stmt USING @user, @dept;

参数化查询结果:

结果:返回 0 条记录(空结果集)。

-- 清理
DEALLOCATE PREPARE stmt;

结果解读:

  • 当输入 ' OR '1'='1 被作为参数绑定时,数据库将其视为一个完整的字符串值,而不是 SQL 表达式。
  • 实际执行的等效查询为:SELECT * FROM employees WHERE emp_name = "' OR '1'='1" AND dept = "' OR '1'='1"。
  • 数据库中不存在员工名为 ' OR '1'='1 且部门为 ' OR '1'='1 的记录,因此返回空结果,查询失败。
  • 这是防范 SQL 注入的唯一正确方法,所有其他方法都只是辅助手段。

方法二:存储过程(辅助手段)

存储过程如果内部使用参数化查询,可以提供额外的安全层。

第一步:创建安全的登录存储过程

DELIMITER //

CREATE PROCEDURE SafeQuery(IN p_emp_name VARCHAR(20), IN p_dept VARCHAR(20))
BEGIN
    -- 存储过程内部仍然使用参数化的 SELECT,不拼接字符串
    SELECT * FROM employees WHERE emp_name = p_emp_name AND dept = p_dept;
END //

DELIMITER ;

第二步:使用正常参数调用

CALL SafeQuery('大翔', '技术部');

正常调用结果:

emp_idemp_namedeptscore
1大翔技术部100

第三步:使用恶意参数调用

CALL SafeQuery(''' OR ''1''=''1', ''' OR ''1''=''1');

恶意调用结果:

结果:返回 0 条记录(空结果集)。

结果解读:

  • 存储过程的输入参数 p_emp_name 和 p_dept 被数据库视为纯数据。
  • 即使存储过程中直接写 WHERE emp_name = p_emp_name,数据库内部仍然按参数绑定处理。
  • 警告:如果存储过程内部拼接 SQL(如使用 CONCAT 构建动态 SQL 再执行),同样会存在注入风险。

第四步:创建不安全的存储过程(反面教材)

DELIMITER //

CREATE PROCEDURE UnsafeQuery(IN p_emp_name VARCHAR(20), IN p_dept VARCHAR(20))
BEGIN
    SET @sql = CONCAT('SELECT * FROM employees WHERE emp_name = ''', p_emp_name, ''' AND dept = ''', p_dept, '''');
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END //

DELIMITER ;

第五步:用恶意参数调用不安全的存储过程

CALL UnsafeQuery(''' OR ''1''=''1', ''' OR ''1''=''1');

不安全调用结果:

emp_idemp_namedeptscore
1大翔技术部100
2白歌技术部NULL

结果解读:

  • UnsafeQuery 存储过程内部使用 CONCAT 拼接 SQL 字符串,与应用程序拼接 SQL 没有本质区别。
  • 攻击者的恶意输入被直接嵌入到 SQL 中,再次成功绕过验证,返回了全部用户数据。
  • 结论:存储过程本身不能防止注入,只有在存储过程内部也使用参数化查询时才安全。

方法三:最小权限原则(数据库层面防御)

即使应用程序存在注入漏洞,数据库用户的权限最小化可以极大降低损失。

第一步:创建不同权限的数据库用户

-- 创建只读用户
CREATE USER 'readonly_app'@'localhost' IDENTIFIED BY 'ReadOnly123!';
GRANT SELECT ON app_db.* TO 'readonly_app'@'localhost';

-- 创建读写用户(无删表权限)
CREATE USER 'readwrite_app'@'localhost' IDENTIFIED BY 'ReadWrite123!';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'readwrite_app'@'localhost';

-- 查看权限
SHOW GRANTS FOR 'readonly_app'@'localhost';
SHOW GRANTS FOR 'readwrite_app'@'localhost';

第二步:模拟只读用户遭受注入攻击

使用 readonly_app 用户连接数据库,尝试执行注入语句:

-- 以 readonly_app 身份执行
USE app_db;

-- 尝试 UNION 注入(只读权限下可以执行)
SELECT * FROM scores WHERE emp_id = 2
UNION
SELECT emp_id, emp_name, score FROM employees;

只读用户注入结果:

score_idemp_idscore
2285
1大翔100
2白歌NULL
-- 尝试 DROP TABLE 注入(只读权限下执行失败)
DROP TABLE scores;

结果:

ERROR 1142 (42000): DROP command denied to user 'readonly_app'@'localhost' for table 'scores'

第三步:模拟读写用户遭受注入攻击

使用 readwrite_app 用户连接数据库:

-- 以 readwrite_app 身份执行
USE app_db;

-- 尝试 DELETE 注入(可以执行,造成数据丢失)
DELETE FROM scores WHERE score_id = 1;

-- 尝试 DROP TABLE 注入(无权限,执行失败)
DROP TABLE scores;

结果:

ERROR 1142 (42000): DROP command denied to user 'readwrite_app'@'localhost' for table 'scores'

结果解读:

  • readonly_app 用户虽然可以执行 SELECT 注入(数据泄露),但无法执行 DROP TABLE、TRUNCATE 等破坏性操作。
  • readwrite_app 用户可以执行 DELETE 注入造成数据丢失,但仍然无法删除表结构。
  • 如果应用程序使用 root 用户连接数据库,注入攻击者将拥有最高权限,可以执行任意操作(包括 DROP DATABASE)。
  • 最小权限原则是 SQL 注入防范的重要辅助防线:即使应用层被突破,数据库层也能限制损失范围。

方法四:输入验证和转义(不可靠,不推荐单独使用)

// 错误示范:手动转义,容易遗漏
emp_name = emp_name.replace("'", "\\'");

MySQL 层面验证转义的局限性:

-- 假设应用程序将单引号转义为 \'
SET @escaped_input = '\\' OR \\'1\\'=\\'1';

-- 某些场景下,转义仍然可能被绕过(如宽字节注入)
-- 这是不推荐依赖手动转义的根本原因

结果解读:

  • 手动转义容易被绕过:宽字节注入、Unicode 编码、注释符 --、#、多语句执行 ; 等技巧都可能使转义失效。
  • 不同的数据库驱动、字符集、SQL Mode 下,转义规则可能不同。
  • 永远不要依赖手动转义,参数化查询是唯一可靠的防御方式。

方法五:启用安全的 SQL Mode

-- 查看当前 SQL Mode
SELECT @@sql_mode;

-- 设置安全的 SQL Mode(禁止某些危险行为)
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_BACKSLASH_ESCAPES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';

验证 NO_BACKSLASH_ESCAPES 的安全意义:

-- 默认模式下,反斜杠是转义字符
SET SESSION sql_mode = '';
SELECT '\\'' AS test;

结果:

test
'
-- 开启 NO_BACKSLASH_ESCAPES 后
SET SESSION sql_mode = 'NO_BACKSLASH_ESCAPES';
SELECT '\\'' AS test;

结果:

test
'

注意:在 NO_BACKSLASH_ESCAPES 模式下,反斜杠不再作为转义字符,这可以防止某些利用反斜杠的注入技巧。

结果解读:

  • NO_BACKSLASH_ESCAPES 使反斜杠 \ 失去转义能力,防止攻击者利用 \' 等技巧绕过转义。
  • 但 SQL Mode 只是辅助措施,不能替代参数化查询。

SQL 注入防范清单

必须做绝不要做
使用参数化查询(PreparedStatement)字符串拼接 SQL
最小权限原则(应用账号只给必要权限)应用使用 root 账号
密码哈希存储(bcrypt/SHA-256 + salt)明文存储密码
启用 SQL Mode:NO_BACKSLASH_ESCAPES信任任何用户输入
日志审计异常查询在生产环境显示详细错误信息

常见误区

误区正解
"我用的是框架,不会有注入"框架提供了安全方法,但如果开发者使用字符串拼接(如某些 ORM 的 raw() 方法),仍然会注入。
"存储过程能完全防止注入"存储过程内部如果拼接 SQL,同样会注入。
"我只是内部系统,不需要防注入"内部系统同样有被渗透的风险,且内部数据往往更敏感。

面试考点

Q:什么是 SQL 注入?如何防范?

SQL 注入是攻击者通过输入恶意 SQL 片段,让应用执行非预期数据库操作的攻击。唯一正确的防范方法是参数化查询(PreparedStatement),将 SQL 和数据分离。辅助措施包括:最小权限原则、密码哈希、输入校验、WAF。

Q:参数化查询为什么能防止注入?

数据库驱动将 SQL 模板和参数分两次发送。数据库先解析和编译 SQL 模板,参数随后作为纯数据绑定,永远不会被当作 SQL 代码解析。即使参数中包含 ' OR '1'='1,也只会被当作普通字符串值处理。

Q:MySQL 的 NO_BACKSLASH_ESCAPES SQL Mode 有什么安全意义?

默认情况下 \' 会被视为转义的单引号。开启 NO_BACKSLASH_ESCAPES 后,反斜杠不再作为转义字符,这可以防止某些利用反斜杠的注入技巧。

小结

  • SQL 注入是用户输入被当作代码执行的攻击
  • 参数化查询是唯一正确的防御方式
  • 最小权限原则、密码哈希、输入校验是辅助防线
  • 永远不要拼接 SQL 字符串,永远不要信任用户输入

下一章引子:安全讲完了,接下来进入数据库性能的核心——如何发现慢查询、分析执行计划、优化索引和 SQL。

上一页
角色