最可靠方式是createcommand()配合bindvalue()等参数绑定,而非拼接字符串;active record仅对固定结构安全,动态字段、排序等仍需白名单校验或手动绑定。

直接用 createCommand() + 参数绑定,是 Yii 防 SQL 注入最可靠的方式;Active Record 虽自动防护,但一碰动态字段、排序、原生子查询就容易漏防。
createCommand() 必须绑定参数,不能拼字符串
常见错误是把用户输入直接塞进 SQL 字符串里,比如:"SELECT * FROM user WHERE name = '" . $_GET['name'] . "'"——这等于给攻击者开了个数据库后门。Yii 的 createCommand() 本身不防注入,防注入靠的是后续的 bindValue() 或 bindValues()。
- 占位符必须带冒号前缀:
:status,不是status或?<code> - 绑定时键名要严格一致:
bindValue(':status', $status),少个冒号就失效 - 字符串类型建议显式指定:
bindValue(':name', $name, PDO::PARAM_STR),避免数字型字段被误判为整数导致截断 - IN 条件要用循环绑定或
str_repeat()拼占位符,不能写成"id IN (" . implode(',', $ids) . ")"
Active Record 不是万能盾,动态部分照样会中招
像 User::find()->where(['status' => $status])->all() 这种写法确实安全,因为 AR 内部走的是预处理。但一旦涉及字段名、表名、ORDER BY、GROUP BY 等动态结构,AR 就不兜底了。
- 排序字段必须白名单校验:
if (!in_array($sort, ['created_at', 'price', 'name'])) { throw new BadRequestHttpException(); } - 不能写:
->orderBy($sort . ' DESC'),而要写:->orderBy([$sort => SORT_DESC])(仅限白名单内字段) - 连表别名、自定义字段别名若来自用户输入,也得先过滤再拼,否则
AS $_GET['alias']就是注入点 - 用
andFilterWhere()代替手拼条件,它会跳过空值且保持绑定逻辑
queryAll() / queryOne() 和 execute() 别混用
这是新手高频翻车点:拿 execute() 去查数据,或者拿 queryAll() 去执行 INSERT——结果要么没数据、要么报错、要么静默失败。
-
execute()只用于无结果集语句:INSERT / UPDATE / DELETE / CREATE / DROP,返回受影响行数(int) -
queryAll()返回二维数组,queryOne()返回一维数组,queryScalar()返回单个值(如 COUNT) - 查 SELECT 却调了
execute(),你会拿到0,但实际什么都没取出来 - INSERT 用
insert('table', $data)更简洁,但含ON DUPLICATE KEY UPDATE时,还是得回createCommand()+execute()
事务里复用 command 对象要注意变量引用
批量操作时想复用一个 createCommand() 实例提升性能,用 bindParam() 比 bindValue() 更合适,但必须注意变量生命周期。
-
bindParam()绑定的是变量引用,不是值快照;循环中修改变量,下次queryOne()就用新值 - 如果在事务闭包外定义了绑定变量,闭包内又没重新赋值,可能拿到的是上一次的旧值
- 更稳妥的做法是:在循环体内每次调
bindValue(),或确保bindParam()所绑变量只在当前迭代内有效 - 事务未提交前,重复执行同一 command 不会自动刷新连接状态,出错时记得手动
rollback()
最危险的不是不会写绑定,而是以为“用了 AR 就安全”或“写了 createCommand 就算防护了”。SQL 注入的入口往往藏在 ORDER BY、字段别名、IN 列表、甚至 LIMIT 的偏移量里——这些地方不校验、不绑定,预处理机制就形同虚设。











