真正防sql注入需用mysqli::prepare()传纯模板(仅含?占位符),再通过bind_param()绑定变量,禁用pdo模拟预处理、校验动态标识符并设置正确字符集。

直接用 mysqli::prepare() 创建语句对象,再调用 bind_param() 和 execute(),才是真正的预处理。不是写个 PREPARE SQL 就算——那只是 MySQL 服务端语法,不防注入。
mysqli::prepare() 必须传纯模板,不能含用户输入
预处理生效的前提是 SQL 字符串里只出现 ? 占位符,所有变量值必须通过后续绑定传入。一旦把 $_GET、$_POST 拼进 SQL 字符串,哪怕后面用了 prepare(),也完全失效。
- 危险写法:
$sql = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'"; $stmt = $mysqli->prepare($sql);—— 拼接已发生,prepare()来不及救 - 正确写法:
$sql = "SELECT * FROM users WHERE name = ?"; $stmt = $mysqli->prepare($sql); - 表名、列名、
ORDER BY字段、UNION分支等无法用?占位,必须白名单校验,例如in_array($sort, ['id', 'created_at'], true)
bind_param() 的类型标识必须匹配实际数据
bind_param() 第一个参数是类型字符串,它控制数据库如何解析传入的值。错配不仅可能绕过校验,还会导致隐式转换或索引失效。
-
"s":字符串,适用于VARCHAR、TEXT等字段;传整数如123会被转成字符串"123" -
"i":整数,适用于INT、BIGINT;传字符串如"1 OR 1=1"会被强制截断为1,但若字段是字符串类型,且 SQL 写成WHERE status = ?,而status是VARCHAR,就可能意外匹配到"1"开头的记录 - 错误示例:
bind_param("i", $username)用于WHERE username = ?(username是VARCHAR)——类型错配,既不安全也不合理
PDO 用户注意:禁用模拟预处理
PDO 默认开启 PDO::ATTR_EMULATE_PREPARES = true,此时 PHP 自己做参数替换,等同于字符串拼接,prepare() 形同虚设。
- 必须显式关闭:
new PDO($dsn, $user, $pass, [PDO::ATTR_EMULATE_PREPARES => false]) - 确认 MySQL 版本 ≥ 5.1.17,否则原生预处理可能不可用
- 命名参数(如
:name)和问号占位符安全等级一致,选哪个纯看可读性,不改变底层机制
执行前别漏掉字符集设置
如果连接未设字符集,宽字节注入仍可能发生——比如 %df%27 在 gbk 下被解析为单引号,绕过 mysqli_real_escape_string(),而预处理本身也会因编码不一致失效。
- 务必在
mysqli连接后立即调用:mysqli_set_charset($mysqli, 'utf8mb4') - 对 PDO,在 DSN 中加
;charset=utf8mb4,或连接后执行$pdo->exec("SET NAMES utf8mb4") - 仅靠
bind_param()不足以兜底字符集问题
预处理不是开关一开就万事大吉的事。真正起作用的是「SQL 模板与数据在协议层彻底分离」这个事实,而它依赖你每一步都不越界:不拼接、不错型、不跳过字符集、不放行动态标识符。最容易被忽略的是 ORDER BY 和 IN 子句里的字段名——它们根本没法参数化,只能靠白名单硬控。











