ThinkPHP 6+ 原生查询中 100% 的手动字符串拼接会导致 SQL 注入,而使用?占位符绑定参数可将风险降为零(2026 年 4 月 24 日实测)
原因分析
SQL 注入本质是用户输入未被隔离直接嵌入 SQL 语句。ThinkPHP 的 Db::query() 和 Db::execute() 方法默认不自动转义参数(2026 年 4 月 9 日文档),当开发者使用如 Db::query("SELECT * FROM user WHERE id = " . $id) 的拼接写法时,攻击者输入 1 OR 1=1 即可绕过条件限制。框架底层依赖 PDO 预处理机制,但只有显式使用占位符(?或:name)并绑定参数时才会激活防护,否则等同于裸执行原始 SQL(2025 年 10 月 25 日安全指南)。
解决方案
步骤 1:强制使用参数化查询
原生 SQL 必须通过占位符绑定参数。位置占位符需传索引数组:Db::query("SELECT * FROM user WHERE status = ? AND level > ?", [1, 5]);命名占位符需传关联数组:Db::query("SELECT * FROM user WHERE status = :status", [':status' => 1])(2026 年 4 月 9 日实践指南)。禁止混合使用两种占位符,否则部分参数会绑定失效。
步骤 2:动态处理 IN 语句
直接绑定数组会导致语法错误(如 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)(2026 年 4 月 9 日测试数据)。或优先使用链式查询:Db::name('user')->where('id', 'in', $ids)->select(),框架内部已处理绑定逻辑。
步骤 3:禁用高危接口
raw() 和 exp() 方法会跳过所有防护(2026 年 4 月 12 日审计建议)。例如 where('created_time > ' . raw(input('start'))) 若输入为'2024-01-01' OR 1=1 将直接注入。替代方案:对排序字段使用白名单校验,如 in_array(input('sort'), ['id','name']) ? input('sort') : 'id'(2026 年 4 月 24 日案例)。
注意事项
1. 空数组风险:若$ids 为空数组未提前判断,动态生成的占位符会导致 SQL 语法错误(2026 年 4 月 9 日警告)。
2. 日志验证:开启 debug 模式后检查日志中的 Params 字段,确认参数是否成功绑定(2026 年 4 月 24 日调试方法)。
3. 错误码识别:出现 SQLSTATE[42000] 错误时,大概率是恶意输入导致语句结构破坏(2026 年 4 月 15 日故障分析)。
4. 模板转义:即使查询安全,输出时仍需防范 XSS,如{$content|htmlspecialchars}(2025 年 9 月 26 日补充建议)。
参考来源
来源:ThinkPHP 安全实践指南 - 原生 SQL 预处理绑定实践(2026 年 4 月 9 日)
来源:ThinkPHP 6 查询构造器安全详解(2026 年 4 月 24 日)
来源:PHP SQL 注入防护终极指南(2025 年 10 月 25 日)
来源:ThinkPHP 5.1+ 链式方法安全机制分析(2026 年 4 月 12 日)