前言
一提到SQL注入的防御,首先想到的就是预编译咯,但是我之前并没有仔细研究过预编译,于是今天来谈一谈预编译是如何防御SQL注入的?预编译真的能防御所有的SQL注入吗?
以php+mysql为例
SQL语句的执行流程
在执行 SQL 语句时,数据库通常经历以下步骤:
解析(Parsing):数据库接收到 SQL 语句后,首先对其进行关键字、标识符、运算符的分解,然后进行解析和语法分析。这一步骤的目的是检查 SQL 语句的语法是否正确,并将其转换为一个解析树或语法树。
预处理(Preprocessing):解析后的 SQL 语句会进行预处理,包括检查语句中涉及的表和字段是否存在,用户是否有足够的权限来执行该操作,同时检查语义的正确性等。
优化(Optimization):在这个阶段,数据库优化器会根据数据库的统计信息和索引等信息,生成一个高效的查询执行计划。优化的目的是选择最优的执行路径,以提高查询性能。
生成执行计划(Execution Plan Generation):优化器生成的执行计划被传递给执行引擎,执行引擎会将其转化为具体的操作步骤和访问路径。
执行(Execution):执行引擎根据生成的执行计划,实际执行 SQL 语句,包括访问数据、进行计算和更新数据等操作。
返回结果(Result Return):执行完成后,结果会被返回给用户。对于查询操作,结果通常是数据集;对于修改操作,结果通常是操作成功或失败的状态。
事务管理(Transaction Management):如果 SQL 语句是在事务中执行的,数据库会在执行完成后根据事务的状态(如提交或回滚)来确保数据的一致性和完整性。
总结起来就是:先进行SQL执行语句的编译,再执行编译好的SQL语句,再反馈执行结果
SQL注入产生的原理
以这样一个查询用户信息的SQL注入为例:
select user_info from users_account where key = $_GET['key']
直接接收GET参数key,从users_account表中查询user_info字段,如果key正确就返回对应用户的个人信息
正常的查询是这样:输入正确key,返回对应的用户信息
用户输入正确的key:123456
select user_info from users_account where key = 123456
但是恶意的查询是这样:输入恶意SQL片段,返回所有用户信息
用户输入SQL注入的pyload:1 or 1=1
select user_info from users_account where key = 1 or 1=1
这样就从语义上,改变了整个SQL语句的执行逻辑,由于where key = $_GET['key']此处用户完全可控,又毫无过滤,于是攻击者可插入SQL语句片段来构造出一个完整的恶意的查询,改变了原来的SQL语句的执行逻辑,改变了语法树,从而达到攻击者的恶意目的
什么是预编译
预编译语句是一种在执行 SQL 语句之前,将 SQL 语句的结构和查询逻辑预先处理的技术。它与直接执行 SQL 语句相比,具有以下优点:
防止 SQL 注入:预编译语句将 SQL 语句的结构和数据分开处理,数据作为参数传递,而不是直接嵌入到 SQL 语句中。这种方式有效地防止了 SQL 注入攻击。
提高性能:当使用预编译语句时,数据库可以缓存和重用查询的执行计划,从而提高查询性能,尤其是在相同的查询结构但不同的参数被多次执行时。
预编译的工作流程
准备语句:SQL 语句的结构被发送到数据库服务器,数据库服务器解析并预处理这些语句,生成一个查询执行计划。
绑定参数:在预编译阶段,参数占位符(如
?
或:param
)被用作数据的占位符,实际的参数值在执行阶段绑定到这些占位符。执行语句:将实际的参数值传递给预编译语句,然后执行查询,数据库使用之前生成的执行计划来处理实际的数据。
获取结果:查询执行后,结果返回给 PHP 脚本,可以进一步处理或显示。
预编译代码示例
使用 PDO 的预编译语句
<?php
// 创建 PDO 实例
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'username', 'password');
// 预编译 SQL 语句
$stmt = $pdo->prepare('select user_info from users_account where `key` = :key');
// 绑定参数
$stmt->bindParam(':key', $key);
// 设置参数值
$key = $_GET['key'];
// 执行查询
$stmt->execute();
// 获取结果
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($results);
?>
使用 MySQLi 的预编译语句
<?php
// 创建 MySQLi 实例
$mysqli = new mysqli('localhost', 'username', 'password', 'testdb');
// 检查连接
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
// 预编译 SQL 语句
$stmt = $mysqli->prepare('select user_info from users_account where `key` = ?');
// 绑定参数
$stmt->bind_param('s', $key);
// 设置参数值
$key = $_GET['key'];
// 执行查询
$stmt->execute();
// 获取结果
$result = $stmt->get_result();
$rows = $result->fetch_all(MYSQLI_ASSOC);
print_r($rows);
// 关闭语句和连接
$stmt->close();
$mysqli->close();
?>
预编译为什么能防御SQL注入
预编译技术是提前写好了SQL语句的语法结构,或者叫"模板",所有的用户输入都只是当作参数插入,导致所有试图改变SQL语法结构的SQL注入payload都只是一个参数而已,无法逃逸出来,无法对SQL的语法结构、执行逻辑造成任何影响
SQL注入之所以能成功是因为,可控之处改变了整个SQL语句的语法结构,改变了整个SQL语句的执行逻辑,也就是可以自由控制"模板"
但是如果使用了预编译,语法结构早已提前设定好,无法被修改或影响其执行逻辑,所有的攻击语句都失效了,都只是一个参数,一串字符串而已
没有使用预编译:1 or 1=1
select user_info from users_account where key = 1 or 1=1
此处的 1 or 1=1,就成功的改变了SQL语句的语法结构,改变了执行逻辑
使用了预编译:1 or 1=1
$stmt = $mysqli->prepare('select user_info from users_account where `key` = ?');
select user_info from users_account where key = 【1 or 1=1】
实际执行时,SQL语句的语法结构早已设定好,无法被影响,这里的【1 or 1=1】仅仅是一个参数,一串字符串,不会被解析为SQL语句的一部分
预编译演示
本地创建测试数据库和表,随意填充一点数据