SQL注入与预编译防御机制详解
1. SQL注入基础概念
SQL注入是一种通过在用户输入中插入恶意SQL代码来操纵数据库查询的攻击技术。当应用程序直接将用户输入拼接到SQL语句中时,攻击者可以构造特殊输入来改变原始SQL语句的意图。
1.1 传统SQL注入示例
$username = $_POST['username'];
$sql = "select fraction from fraction where name = '$username';";
攻击payload: 1'union select database();#
这种直接将用户输入拼接到SQL语句中的做法存在严重安全隐患。
2. 预编译技术原理
预编译(Prepared Statements)最初是为了提高MySQL运行效率而设计,但由于其先构建语法树后带入查询参数的特性,使其具有了防止SQL注入的能力。
2.1 MySQL预编译执行流程
-
准备阶段:构建语法树
prepare sel from "select fraction from fraction where name = ?"; -
设置变量
set @a='mechoy'; -
执行阶段:将变量代入已构建的语句
execute sel using @a;
2.2 预编译防注入原理
预编译将用户输入视为数据而非SQL代码的一部分。即使输入中包含SQL关键字或特殊字符,数据库也会将其作为普通字符串处理,不会改变原始SQL语句的结构。
3. PHP中的预编译实现
PHP中主要有两种方式连接MySQL数据库并实现预编译:
3.1 使用MySQLi扩展
$stmt = mysqli_stmt_init($conn);
mysqli_stmt_prepare($stmt,"select fraction from fraction where name = ?");
mysqli_stmt_bind_param($stmt,"s", $username);
mysqli_stmt_execute($stmt);
3.2 使用PDO扩展
$stmt = $conn->prepare("select fraction from fraction where name = :username");
$stmt->bindParam(":username",$username);
$stmt->execute();
3.2.1 PDO的模拟预编译与真实预编译
PDO默认使用模拟预编译,即在客户端模拟参数绑定过程,最终发送完整SQL语句到数据库:
// 默认模拟预编译
$conn = new PDO($dbs, $dbname, $passwd);
要使用数据库原生预编译,需设置:
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
4. 预编译的局限性及绕过方法
尽管预编译能有效防止大多数SQL注入,但在某些特殊情况下仍可能存在安全隐患。
4.1 PDO模拟预编译+宽字节注入
当使用GBK等宽字符集且未正确设置时:
$dbs = "mysql:host=127.0.0.1;dbname=sort1;charset=gbk";
$conn->query('SET NAMES GBK');
攻击payload: 1%df%27%20union%20select%20database();#
利用%df吃掉转义斜杠,造成注入。
4.2 PDO的错误使用方式
错误地将用户输入直接拼接到预编译语句中:
$stmt = $conn->prepare("select fraction from fraction where name = '$username'");
这等同于直接拼接,完全失去了预编译的保护作用。
4.3 PDO的多条语句执行
PDO默认支持多条SQL语句执行,可能导致堆叠注入:
$stmt = $conn->prepare("select fraction from fraction where id=$id");
攻击payload: 1;select%20database()
4.4 LIKE子句中的预编译问题
直接在LIKE中使用预编译参数可能无效:
$stmt = $conn->prepare("select * from fraction where name like '%:username%'");
正确做法:使用CONCAT函数
select * from fraction where name like concat('%',:username,'%')
5. ORDER BY子句的特殊情况
ORDER BY后的参数通常不能使用预编译,因为:
5.1 排序字段名问题
$stmt = $conn->prepare("select * from fraction order by :col");
实际执行的是order by 'fraction'(字符串),而非order by fraction(字段名)。
5.2 ASC/DESC排序方向问题
$stmt = $conn->prepare("select * from fraction order by fraction :asc");
这会抛出语法错误,导致开发者不得不使用拼接。
5.3 ORDER BY注入技术
当ORDER BY使用拼接时,可利用以下技术:
-
报错注入
order by 1 and 1=updatexml(0,concat('~',user(),'~'),1)# -
延时盲注
order by if(1,sleep(3),sleep(0))# -
布尔盲注
order by if((user()='root@localhost'),fraction,id);
6. 防御建议
- 优先使用预编译语句处理所有用户输入
- 使用PDO时明确设置
ATTR_EMULATE_PREPARES为false - 对于必须拼接的情况(如ORDER BY字段名),采用白名单验证
- 正确设置数据库字符集,防止宽字节注入
- 避免直接拼接用户输入到SQL语句中,即使使用了预编译框架
- 对LIKE等特殊子句使用正确的预编译写法
7. 总结
预编译是防止SQL注入的有效手段,但并非万能。开发者需要理解其工作原理和局限性,在无法使用预编译的特殊情况下采取其他安全措施。安全开发需要综合考虑各种场景,不能仅依赖单一防御机制。