预编译语句防注入的关键是参数化绑定而非PREPARE语法本身;mysqli_prepare()通过分离模板与参数并独立传输确保安全,而手写PREPARE+动态SQL仍易遭注入。

预编译语句(PREPARE / EXECUTE)本身不是防注入的银弹,真正起作用的是参数化绑定——即用占位符代替拼接,由数据库驱动层确保输入不参与语法解析。直接手写 PREPARE + EXECUTE 动态 SQL 反而容易引入新风险。
为什么 mysqli_prepare() 比手写 PREPARE 安全得多
PHP 的 mysqli_prepare() 不是简单封装 SQL 的 PREPARE 语句,它在底层强制分离「查询模板」和「数据值」:模板送入 MySQL 服务端预编译,参数通过独立协议通道传入,MySQL 不会对参数做任何 SQL 解析。而纯 SQL 层的 PREPARE stmt FROM @sql 中,如果 @sql 来自用户输入,就等于又回到了字符串拼接的老路。
-
mysqli_prepare("SELECT * FROM users WHERE id = ?"):安全。问号被 MySQL 视为类型化参数占位符 -
SET @sql = CONCAT("SELECT * FROM users WHERE id = ", $_GET['id']); PREPARE stmt FROM @sql;:极度危险。用户控制了整个 SQL 字符串
bind_param() 的类型标识不能乱写
bind_param() 第二个参数起必须严格匹配实际数据类型,否则可能绕过类型校验或触发隐式转换漏洞:
-
"i"表示整数:传入"1 OR 1=1"会被强制转成1,但若字段是字符串类型(如VARCHAR),且没加引号,仍可能出问题 -
"s"表示字符串:对单引号、反斜杠等自动转义,但仅限于该参数上下文;不能依赖它来“过滤”整个字段内容 - 错误示例:
bind_param("i", $user_input)用于WHERE name = ?(name 是字符串字段)——类型错配可能导致索引失效或意外匹配
LIKE 查询里的 % 和 _ 需要单独转义
预编译能防止注入,但无法阻止通配符滥用。用户输入 "admin%" 进 WHERE username LIKE ?,会查出所有 admin 开头的账号——这不是注入,但属于业务逻辑越权。
- 正确做法:用
addcslashes($input, '_%\')转义通配符,再拼进LIKE模式,例如WHERE username LIKE CONCAT('%', ?, '%') - 别用
ESCAPE子句配合用户可控的 escape 字符,那又开了新入口 - 如果业务允许模糊搜索,建议限制最大长度(如
substr($input, 0, 20))并拒绝含%、_的原始输入
预编译不是万能的,边界场景要额外设防
表名、列名、排序字段(ORDER BY)、UNION 分支这些无法参数化,一旦来自用户输入,预编译完全失效:
- 错误:
$order = $_GET['sort']; $stmt = $mysqli->prepare("SELECT * FROM logs ORDER BY $order"); - 正确:用白名单校验,例如
in_array($order, ['created_at', 'status'], true) ? $order : 'id' - 动态
UNION查询必须拆成多个固定语句分支,用if/else控制流程,绝不在 SQL 字符串里拼接
最易被忽略的一点:预编译只保 SQL 语句本身,不保后续的 PHP 字符串拼接。比如 mysqli_real_escape_string() 处理后的结果再塞进 prepare(),反而多此一举,还可能因两次转义导致查不到数据。信任预编译,就彻底丢掉手动转义。











