MySQL预编译防SQL注入因参数与SQL结构分离,服务端执行时才绑定值,不参与语法解析;但表名、字段名等动态结构仍需白名单校验,且PDO须禁用模拟预编译。

MySQL预编译语句为什么能防SQL注入
因为参数值不会被拼进SQL字符串,而是由MySQL服务端在执行阶段单独绑定,语法解析和参数代入完全分离。哪怕传入 ' OR 1=1 -- 这种字符串,它也只会当做一个普通文本值处理,不会改变SQL结构。
常见错误现象:用字符串拼接构造查询,比如 "SELECT * FROM users WHERE name = '" + userInput + "'",用户输 ' OR '1'='1 就直接绕过条件。
- 必须用支持预编译的驱动(如MySQLi的
prepare()、PDO的prepare()),原生mysql_query()(已废弃)不支持 - 占位符只能用于数据值,不能用于表名、字段名、ORDER BY子句——这些必须走白名单校验或硬编码
- MySQLi中使用
?占位符;PDO支持?和命名参数如:name,但命名参数在MySQL里底层仍转为?
PHP中MySQLi预编译语句的正确写法
关键不是“用了prepare”,而是“参数是否真正脱离SQL字符串”。很多人写了 prepare() 却还在 bind_param() 前手动拼接变量,等于白做。
正确示例:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$stmt = $mysqli->prepare("SELECT id, name FROM users WHERE status = ? AND age > ?");
$stmt->bind_param("si", $status, $age); // "si" 表示 string + int
$status = "active";
$age = 18;
$stmt->execute();
$result = $stmt->get_result();
-
bind_param()的类型字符串必须严格匹配参数数量和顺序:"s"(string)、"i"(integer)、"d"(double)、"b"(blob) - 不要对变量二次转义(比如再调用
mysqli_real_escape_string()),预编译下这反而可能污染原始值 - 如果参数来自数组,需用
call_user_func_array()拆包,不能直接传数组给bind_param()
PDO预编译与错误配置导致失效的坑
PDO默认开启模拟预编译(PDO::ATTR_EMULATE_PREPARES = true),此时SQL仍在PHP层拼接,只是“假装”预编译,根本防不住注入。
必须显式关闭模拟:
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
- 仅设置
PDO::ATTR_EMULATE_PREPARES => false不够,还要确认MySQL服务端版本 ≥ 5.1.17(旧版不支持原生预编译) - 某些云数据库(如阿里云RDS低配版)默认禁用预编译,连接时会静默回退到模拟模式,建议执行
SELECT @@session.prepared_stmt_count验证 - 使用命名参数时,键名不能含空格或特殊字符,
:user_name可以,:user name会报SQLSTATE[HY093]
哪些场景不能靠预编译,必须另加防护
预编译只保“值”,不保“结构”。动态表名、字段名、LIMIT偏移量、GROUP BY字段这些,没法用 ? 占位,硬塞进去会直接报错。
- 表名/字段名:必须从预设白名单中选取,例如
$allowed_tables = ['users', 'orders']; if (!in_array($table, $allowed_tables)) die('invalid table'); - LIMIT参数:数字型可先强制转为int,但注意
intval("10 OFFSET 100")仍得10,应配合正则/^\d+$/校验 - ORDER BY字段:不能只校验是否为字母,要限定为已知字段集,且排除
id ASC, name DESC这类复合写法(除非你明确允许) - JSON字段路径(如
data->'$.email'):路径部分属于SQL语法,需白名单或严格格式校验,不能参数化
最常被忽略的是:把用户输入直接拼进 IN (?) 占位符——一个 ? 只对应一个值,IN 多个值必须生成多个 ? 并逐个绑定,否则要么报错,要么逻辑出错。











