必须设为false,否则预处理形同虚设;因模拟模式(true)在PHP层拼接SQL,绕过数据库原生预处理,无法实现参数与语句的物理隔离,导致SQL注入风险未消除。

PDO::ATTR_EMULATE_PREPARES 必须设为 false,否则预处理语句形同虚设,SQL注入风险仍在。
为什么 PDO::ATTR_EMULATE_PREPARES=false 是硬性要求
模拟预处理(true)会让 PDO 在 PHP 层自行解析 SQL、拼接参数,数据库收到的是一条完整字符串。这不仅绕过 MySQL 的查询计划缓存,更关键的是:某些边界场景下(如含单引号的字符串、多语句、特殊字符组合),PDO 的模拟逻辑可能误判占位符边界,导致参数未被完全隔离。
原生预处理(false)则把 SQL 模板和参数分两次发给 MySQL,由服务端完成编译与绑定——这是防注入的物理隔离层,无法被客户端逻辑绕过。
- MySQL 5.6+、MariaDB 10.2+ 均稳定支持原生预处理,无需兼容旧驱动
- PHP 7.4+ 默认仍开启模拟,不能依赖版本升级自动修复
-
mysqlnd驱动必须配合emulate_prepares=false才能启用原生 prepare
CI 环境中配置 PDO::ATTR_EMULATE_PREPARES 的实操要点
CodeIgniter 4 默认使用 PDO,但其数据库配置不暴露 ATTR_EMULATE_PREPARES 字段,需手动干预连接过程。直接改 app/Config/Database.php 不生效,因为 CI 在创建连接时未透传该选项。
正确做法是在自定义数据库扩展或连接后置钩子中强制设置:
// app/Database/MySQLiHandler.php 或独立初始化脚本 $pdo = $this->connID; $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
- 务必在首次执行
prepare()前调用setAttribute,否则已建立的连接不会重协商协议 - 若使用连接池或长连接(如 Swoole),每次复用连接后都应检查并重置该属性
- CI 4.4+ 可通过
['DSN' => 'mysql:...;charset=utf8mb4', 'options' => [...]]传入PDO::ATTR_EMULATE_PREPARES,但需确认底层调用未覆盖
验证是否真正启用了原生预处理
光看配置不等于生效。最可靠的方式是抓包或查 MySQL 状态:
在 MySQL 中执行:SHOW STATUS LIKE 'Com_stmt%';,然后触发一次 CI 的查询。若 Com_stmt_prepare 计数增加,且 Com_stmt_execute 随之增加,说明走的是原生 prepare 流程;若只有 Com_query 变动,则仍是模拟模式。
- 开发环境可临时开启 MySQL general log:
SET GLOBAL general_log = 'ON';,观察日志中是否出现Prepare和Execute分离记录 - 不要依赖
$stmt->queryString输出判断——模拟模式下它也显示带问号的原始 SQL - CI 的
$builder->getCompiledSelect()返回的是模拟拼接结果,不能反映真实传输行为
容易被忽略的兼容性陷阱
关闭模拟后,部分 SQL 写法会报错,这不是 bug,而是原生协议对语法的严格校验:
-
IN (?)不合法,必须写成IN (?, ?, ?)并动态生成占位符数量 - 表名、字段名不能参数化,
ORDER BY ?会失败,需白名单过滤后拼接 - MySQL 8.0+ 对
JSON_CONTAINS等函数的参数类型更敏感,需确保绑定值类型匹配(如用PDO::PARAM_STR而非默认的PDO::PARAM_STR自动推断)
这些限制恰恰是安全边界的体现——它们堵死了“用参数控制 SQL 结构”的灰色路径。妥协方案不是开回 emulate_prepares=true,而是重构查询逻辑或引入白名单映射。


















