必须使用bind()绑定参数而非字符串拼接,否则存在SQL注入风险;位置占位符用索引数组绑定,命名占位符用关联数组绑定;IN语句需动态生成占位符或用链式查询;PDO模拟预处理必须关闭。

ThinkPHP原生SQL里不加bind()就直接拼字符串?危险
这是最常见也最致命的写法:用Db::query("SELECT * FROM user WHERE id = " . $id),哪怕$id看着是数字,只要没经过严格类型校验或强制转换,攻击者就能塞进1 OR 1=1或者更复杂的注入载荷。
ThinkPHP的Db::query()和Db::execute()本身不自动转义,也不做参数绑定——它就是把字符串原样扔给PDO。你得自己负责安全。
- 必须用
bind()方法显式绑定参数,而不是字符串拼接 - 绑定时优先用
PDO::PARAM_STR或PDO::PARAM_INT指定类型,别依赖自动推断 - 如果参数来自
$_GET或$_POST,先做基础过滤(比如intval()或filter_var($x, FILTER_SANITIZE_NUMBER_INT)),再进绑定流程
用bind()时传数组还是单个变量?看SQL占位符类型
ThinkPHP支持两种占位符:?(位置占位)和:name(命名占位)。它们对应不同的bind()用法,混用会失效。
位置占位适合简单查询,但容易错位;命名占位可读性好,尤其在多个同类型参数(比如多个IN值)时更可控。
立即学习“PHP免费学习笔记(深入)”;
- 用
?时,bind()必须传索引数组:Db::query("SELECT * FROM user WHERE status = ? AND level > ?", [1, 5]) - 用
:status和:level时,bind()必须传关联数组:Db::query("SELECT * FROM user WHERE status = :status AND level > :level", [':status' => 1, ':level' => 5]) - 别写
Db::query("...", [1, ':level' => 5])这种混合结构——它不会报错,但部分参数根本不会被绑定
IN语句怎么安全绑定多个值?不能直接bind(['a','b','c'])
直接把数组传给bind(),ThinkPHP默认把它当一个整体参数处理,最终生成的SQL会是WHERE id IN ('a,b,c'),而不是WHERE id IN ('a','b','c')——这既语法错误,又完全绕过预处理本意。
正确做法是动态生成占位符,并确保每个值单独绑定。
- 手动拼占位符串:
$ids = [1,2,3]; $placeholders = str_repeat('?,', count($ids) - 1) . '?'; Db::query("SELECT * FROM user WHERE id IN ($placeholders)", $ids); - 用
Db::name('user')->where('id', 'in', $ids)->select()这类链式调用更省心——它内部已处理好IN绑定逻辑 - 如果非要用原生SQL且
$ids为空数组,记得提前判断,否则IN ()会触发SQL语法错误
开启PDO的PDO::ATTR_EMULATE_PREPARES会影响绑定效果吗?
ThinkPHP默认关闭模拟预处理(即ATTR_EMULATE_PREPARES = false),这是对的。一旦设为true,PDO会在客户端“假装”预处理,把参数拼回SQL再发给MySQL——等于白绑,SQL注入风险照旧。
有些老项目为了兼容低版本MySQL(如5.5之前不支持某些预处理语法)会开这个开关,但现在基本没必要。
- 检查配置:
'params' => [PDO::ATTR_EMULATE_PREPARES => false]必须存在且为false - 如果用了MySQL 5.7+ 或 MariaDB 10.2+,无需妥协,坚持关掉模拟预处理
- 开启后即使写了
bind(),抓包看实际发送的SQL,很可能发现参数已被拼进去了
真正起作用的是PDO底层是否把参数和SQL分开传输。绑定只是第一步,底层驱动配合不上,一切归零。


















