pdo::prepare必须用占位符(?或:name)配合bindvalue()或execute()传参,禁用字符串拼接;问号占位用数字索引,命名占位用关联数组;bindparam因引用绑定易出错,应避免;连接需显式指定charset=utf8mb4防宽字节注入。

pdo::prepare 里不能直接拼接变量
PHP 的 PDO::prepare 不是字符串模板,把变量直接塞进 SQL 字符串里(比如 "SELECT * FROM user WHERE id = $id")就彻底废了预处理的意义——SQL 结构没固定,数据库根本没法缓存执行计划,更关键的是,攻击者仍能通过 $id 注入恶意内容。
正确做法是用占位符,让 PDO 在底层做类型化绑定:
- 用
?(问号占位)或:name(命名占位)代替值,SQL 字符串必须是静态的 - 值只允许通过
bindValue()或execute()的参数数组传入 - 不要对占位符做字符串替换、
str_replace或sprintf
bindValue 和 execute 传参的区别在哪
两者都能安全绑定,但时机和灵活性不同:bindValue 是显式绑定,适合需要复用同一语句多次执行、且每次绑定类型/值可能变化的场景;execute() 接数组是快捷写法,适合单次执行,且数组键名必须严格匹配命名占位符(:id)、或按顺序对应问号占位。
常见错误现象:execute([':id' => 123]) 对问号占位无效,会报 Invalid parameter number;反过来,用 bindValue(1, $id) 绑定命名占位也会失败。
- 问号占位 → 用数字索引数组传给
execute(),或bindValue(1, $val) - 命名占位(如
:user_id)→ 用关联数组传给execute(),或bindValue(':user_id', $val) -
bindValue()可指定第三个参数(如PDO::PARAM_INT),execute()数组里的值默认当字符串处理(除非提前设setAttribute(PDO::ATTR_EMULATE_PREPARES, false))
bindParam 为什么多数时候不该用
bindParam 绑定的是变量“引用”,不是值。它在 execute() 时才读取变量当前值——这听起来灵活,实则极易出错。
典型翻车场景:循环中复用同一个 $stmt,用 bindParam 绑定一个在循环里反复赋值的变量,最后所有执行都查到的是最后一次循环的值(因为引用指向没变)。
- 除非你明确需要“延迟读值”且能控制变量生命周期,否则一律用
bindValue或execute()数组 -
bindParam的第二个参数必须是变量(不能是表达式或字面量),写bindParam(1, $id + 1)会报 PHP Warning - 绑定后若修改该变量值,再
execute()就会用新值——这点常被误当成“特性”,其实是陷阱源头
字符集不一致照样导致注入绕过
即使 SQL 用对了预处理,如果 MySQL 连接字符集(如 utf8mb4)和 PHP 传入字符串编码不一致,某些多字节编码下仍可能触发宽字节注入(例如 %df%27 被 MySQL 当作单字节 ' 处理)。
- 连接 DSN 必须显式指定字符集:
mysql:host=localhost;dbname=test;charset=utf8mb4 - 不要依赖
SET NAMES,它不保证预处理通道的编码一致性 - 检查
PDO::getAttribute(PDO::ATTR_CLIENT_VERSION)和实际 MySQL 版本,5.5.3+ 才完整支持utf8mb4
安全不是靠某一行代码,而是连接、准备、执行、编码四者对齐。少一个环节,prepare 就只是个看起来很美的函数名。









