原生查询必须使用参数绑定,禁用模拟预处理并校验标识符。ThinkPHP中query()/execute()需用?或:name占位符绑定值;表名/字段名须白名单校验;关闭PDO::ATTR_EMULATE_PREPARES防止拼接。

原生查询不加参数绑定=直接裸奔
ThinkPHP 的 query() 和 execute() 默认不做任何 SQL 解析或转义,传进去的字符串原样送进 PDO。哪怕你用 input('id') 拼在 SQL 里,攻击者输个 1; DROP TABLE user; 就能触发多语句执行(取决于 PDO 配置)。
- 别信“我只查数字就安全”——
intval()漏掉负号、科学计数法、空格绕过很常见 - 别手动
addslashes()或str_replace()——PDO 的预处理机制和 MySQL 实际协议层绑定才是唯一可靠路径 - MySQL 8.0+ 默认禁用多语句,但旧项目或配置为
PDO::MYSQL_ATTR_MULTI_STATEMENTS => true时仍危险
所有原生查询必须用 query() / execute() 的参数绑定形式
ThinkPHP 支持两种绑定写法,本质都是调用 PDO::prepare + execute,但语法位置容易错:
- 占位符用
?:顺序绑定,适合简单场景,例如$db->query("SELECT * FROM user WHERE id = ? AND status = ?", [$id, $status]) - 占位符用
:name:命名绑定,可复用、易读,例如$db->query("SELECT * FROM user WHERE id = :id AND role = :role", ['id' => $id, 'role' => $role]) - 注意:
where()链式调用里的数组条件(如where(['id' => $id]))是 ThinkPHP 自己处理的,不属于原生绑定,别混用
拼接表名/字段名时不能用参数绑定,必须白名单校验
PDO 绑定只支持值(value),不支持标识符(identifier)。所以 "SELECT * FROM {$table}" 或 "ORDER BY {$field}" 这类操作,参数绑定完全无效。
- 硬编码白名单最稳:比如排序字段只允许
['id', 'create_time', 'status'],用in_array($field, $allowFields)判定 - 用
think\db\Query::parseSql()不安全——它只是字符串替换,没做标识符转义 - 别依赖正则过滤字母数字下划线:MySQL 支持反引号包裹的任意字符,
`user``test`是合法表名,正则拦不住
开启 PDO 的 PDO::ATTR_EMULATE_PREPARES 关闭模式
ThinkPHP 默认关闭模拟预处理('emulate_prepare' => false),但如果你改过数据库配置或用了旧版驱动,可能意外开启。一旦开启,PDO 会把绑定参数拼成字符串再发给 MySQL,等于白绑。
立即学习“PHP免费学习笔记(深入)”;
- 检查配置项:
'params' => [PDO::ATTR_EMULATE_PREPARES => false] - 验证是否生效:在绑定查询后执行
echo $db->getRealSql();,如果看到实际值(如WHERE id = 123)而不是WHERE id = ?,说明被模拟了 - MySQL 低版本(
真正难的不是写对那行 query(),而是每次拼 SQL 前下意识问一句:这个变量到底进的是 value 还是 identifier?前者绑,后者筛。漏掉一次,整条链就断了。



















