唯一杜绝SQL注入的可靠方式是全程复用CI内置查询绑定能力,而非手动拼接SQL或调用原生驱动;自定义类应注入$this->db实例并使用query()、where()等自动绑定方法,绕不开原生PDO时必须显式prepare+bindValue。

CodeIgniter(CI)中自定义类执行 SQL 时,只要绕过 $this->db 的参数化机制、直接拼接字符串,就等于主动打开 SQL 注入大门。真正杜绝注入的唯一可靠路径,是全程复用 CI 内置的查询绑定能力,而不是自己造轮子。
为什么不能在自定义类里手写 mysqli_query() 或 pdo->exec()
很多开发者以为“自己控制连接对象更安全”,结果在自定义数据库操作类里直接用原生驱动执行拼接 SQL:
// ❌ 危险示例:手动拼接,完全暴露 $sql = "SELECT * FROM users WHERE status = '" . $status . "' AND role = '" . $role . "'"; $this->pdo->exec($sql);
这种写法彻底抛弃了 CI 的绑定层,所有输入未经任何隔离就进入 SQL 解析流程。哪怕加了 htmlspecialchars() 或简单 str_replace(),也防不住 1' OR '1'='1 这类绕过手段。
- CI 的
$this->db实例本身已封装了预编译+参数绑定逻辑,底层调用的就是 PDO 或 MySQLi 的prepare()/execute() - 自定义类若需执行查询,应接收并透传
$this->db实例,而非新建连接 - 若必须脱离 CI DB 类(如对接遗留存储过程),则必须自行调用
prepare()+bindValue(),且禁止任何形式的字符串插值
如何在自定义类中正确使用 CI 的绑定语法
关键不是“能不能用”,而是“怎么把 CI 的绑定能力带进去”。推荐做法:将 $this->db 作为依赖注入进你的类,然后调用其 query() 或 get_where() 等方法。
// ✅ 正确示例:复用 CI 绑定机制
class User_report {
protected $db;
<pre class="brush:php;toolbar:false;">public function __construct($db) {
$this->db = $db; // 接收 CI 的 db 实例
}
public function get_active_by_dept($dept_id, $limit = 10) {
$sql = "SELECT u.id, u.name, d.title
FROM users u
JOIN departments d ON u.dept_id = d.id
WHERE u.status = ? AND u.dept_id = ?";
return $this->db->query($sql, [$status, $dept_id])->result();
}}
-
$this->db->query()第二个参数是数组,CI 会自动按顺序绑定到?占位符,等价于底层prepare/execute - 支持命名占位符:
WHERE u.status = :status,传参用关联数组[':status' => $status] - 不建议在自定义类中调用
$this->db->escape()手动转义——它只是字符串替换,不能替代参数化,且易漏、易双写
CI 4 中 Query Builder 在自定义类里的安全调用方式
CI 4 的 Query Builder 更强调链式调用和隐式绑定,但自定义类里容易误用 where() 的字符串形式。
// ❌ 错误:字符串 where 仍可能拼接
$this->builder->where("name = '" . $name . "'");
<p>// ✅ 正确:始终用数组或独立参数
$this->builder->where('name', $name); // 自动绑定
$this->builder->where(['status' => $status]); // 多条件自动绑定
$this->builder->like('email', $domain, 'after'); // like 也支持绑定
</p>
- 所有
where()、or_where()、having()等方法,只要传入两个参数(字段名 + 值),CI 就会启用参数绑定 - 若必须动态构建条件,用数组传入:
where(['type' => $type, 'level >=' => $level]),全部自动绑定 - 避免使用单参数字符串形式的
where(),那是留给极少数元编程场景的,不是为业务逻辑设计的
绕不开原生 PDO 时,必须手动 prepare 的最小安全单元
极少数情况(如调用含 OUT 参数的存储过程、批量执行非标准语法),你不得不脱离 CI DB 层。此时安全底线只有一条:绝不拼接,只绑定。
// ✅ 强制最小单元:prepare + bindValue
$stmt = $this->pdo->prepare("CALL get_user_summary(?, ?)");
$stmt->bindValue(1, $user_id, PDO::PARAM_INT);
$stmt->bindValue(2, $year, PDO::PARAM_STR);
$stmt->execute();
- 必须用
bindValue()或bindParam(),不能靠execute([$user_id, $year])简写——后者在某些 PDO 驱动下可能退化为模拟预处理(emulated prepares),失去防护能力 - 显式声明参数类型(
PDO::PARAM_INT/PDO::PARAM_STR)能进一步约束数据形态 - 如果 CI 配置中开启了
emulate_prepared_statements = true(MySQL 默认开启),需在 DSN 中强制关闭:mysql:dbname=test;host=localhost;emulate_prepared_statements=false
真正难的不是“会不会写 prepare”,而是每次执行 SQL 前,是否下意识检查了数据来源和拼接痕迹。CI 提供了完整的绑定链路,但只要你跳出它的调用约定,就得自己扛起整套预编译逻辑——而多数人连 bindValue() 的第三个参数都记不全。











