必须关闭pdo模拟预处理,否则预编译仅在php层字符串拼接,既不防注入也不提升性能;默认true导致prepare()不发往数据库,绑定失效、执行计划无法缓存,高并发下性能与安全双崩。

必须关闭PDO模拟预处理,否则所谓“预编译”只是PHP层字符串拼接,既不防注入也不提升性能。
为什么PDO::ATTR_EMULATE_PREPARES = true会让防护和性能双崩
这个配置默认为true(尤其在MySQL驱动下),导致PDO::prepare()根本不发往数据库,而是在PHP内存里做字符串替换——等同于手写"WHERE id = '$id'"。所有绑定参数都失效,SQL注入风险回归;同时服务端无法缓存执行计划,高并发下每条查询都重新解析、生成新计划,pg_stat_statements里calls极低但total_time飙升。
- 确认当前值:
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES),非false即踩坑 - 显式关闭:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false),且必须在new PDO()之后立即执行 - DSN中强制指定字符集:
mysql:host=localhost;dbname=test;charset=utf8mb4,避免因字符集不一致触发隐式类型转换,破坏参数绑定语义
bindValue比execute([$value])更可靠,尤其在PHP 7.4+
用execute([$value])依赖PHP自动推断类型,遇到null、'0'或空字符串时,可能把整数字段当成字符串传入,导致MySQL执行计划无法复用(例如WHERE status = ?传入'0'而非0,索引失效)。
- 数字类参数一律用
bindValue($param, $value, PDO::PARAM_INT) - 布尔值用
PDO::PARAM_BOOL,避免true被转成1再当字符串处理 - NULL值显式传
PDO::PARAM_NULL,不靠execute([null])模糊匹配
statement生命周期失控是高并发下“MySQL server has gone away”的主因
不是网络问题,是MySQL侧prepared statement句柄泄漏。每个PDO::prepare()都会在服务端创建一个句柄,上限由max_prepared_stmt_count控制(默认16382)。循环内反复prepare同一SQL却不释放,很快打满上限,后续所有prepare直接失败。
- 用完立刻
unset($stmt),别等GC;尤其在长连接池(PHP-FPM持久连接、Swoole协程)中必须手动清理 - 确认
wait_timeout与应用层连接空闲超时匹配,否则连接被MySQL主动断开后,应用仍持旧连接调prepare,报Commands out of sync - Swoole场景禁止跨协程共享
PDO或PDOStatement实例——它们非协程安全,必须per-request新建或从连接池取隔离实例
动态表名/字段名不能参数化,白名单校验是唯一出路
PDO和mysqli都不支持把表名、字段名、排序方向(ASC/DESC)作为参数绑定。试图用WHERE ? = ?或ORDER BY ?会直接报错或被忽略。
- 字段映射白名单:
$allowedCols = ['created_at', 'status', 'username']; $col = in_array($input, $allowedCols, true) ? $input : 'id'; - 排序方向硬编码:
$dir = strtolower($input) === 'desc' ? 'DESC' : 'ASC'; $sql = "ORDER BY $col $dir";(注意此处是字符串拼接,但输入已受限) - ORM中警惕
whereRaw、DB::raw、模型accessor里的原生拼接——这些绕过所有参数绑定,whereRaw('name = ?', [$name])才安全,whereRaw("name = '$name'")等于裸奔
真正难的不是写对一条prepare,而是确保整个调用链——从连接初始化、字符集设定、参数绑定方式、statement释放时机,到动态标识符校验——全部落在安全路径上。任何一环松动,高并发下漏洞会以性能退化(慢查询、连接中断)的形式先暴露,而不是报错。











