ThinkPHP插件必须严格防范SQL注入:where条件须用数组或闭包、原生SQL强制占位符绑定、动态表名/字段名需白名单校验、插件数据库配置须禁用PDO模拟预处理。

ThinkPHP插件中若存在动态SQL拼接、未校验的用户输入字段或绕过绑定的raw/exp调用,会直接导致主应用数据库被拖库或删库——这不是潜在风险,而是已知攻击面。
插件内where条件必须用数组或闭包
插件代码常从控制器接收参数后直接构造查询,此时若用字符串拼接,框架完全不介入防护。例如插件中写where("category = " . input('cat')),攻击者传入1 OR 1=1即可绕过所有条件。
改用数组格式:where(['category' => input('cat'), 'status' => 1]),框架自动触发PDO prepare+bindValue流程,值与结构彻底分离。
复杂逻辑必须封装为闭包:where(function ($q) use($keyword, $type) { $q->where('title', 'like', '%' . $keyword . '%')->where('type', $type); }),内部仍走绑定路径,不会退化为文本替换。
立即学习“PHP免费学习笔记(深入)”;
【禁止在插件中出现任何双引号内含input()变量的写法,如"status = " . input('s')】
插件调用原生SQL时强制占位符+参数数组
插件常需执行统计类原生SQL,Db::query()和Db::execute()本身不防注入,必须人工绑定。
方法一(位置占位符):Db::query("SELECT COUNT(*) FROM {$table} WHERE create_time >= ? AND uid IN (?)", [$start, $uids]);
方法二(命名占位符):Db::query("SELECT * FROM plugin_log WHERE level = :level ORDER BY :field DESC", [':level' => 'error', ':field' => $sort]);
注意:命名占位符在ThinkPHP 6.0默认配置下需确认'params_bind' => true已启用,否则:field会被忽略;更稳妥的是统一用?配合索引数组。
⚠️ $uids是数组时,不能直接传入bind,必须先生成动态占位符:$in = str_repeat('?,', count($uids) - 1) . '?';再执行Db::query("uid IN ($in)", $uids)。
插件中动态表名/字段名必须白名单校验
插件支持多租户或自定义字段时,常需拼接表名或ORDER BY字段,但PDO绑定对标识符无效,拼错即高危。
第一步:提取用户输入字段名,如$inputSort = input('sort');
第二步:严格白名单匹配:in_array($inputSort, ['id', 'create_time', 'score', 'status']) ?: 'id';白名单必须硬编码,不可从数据库读取或配置文件加载。
第三步:拼入SQL前做二次断言:【表名必须用反引号包裹,且仅允许字母、数字、下划线,如`{$table}`】;禁止使用think\db\Query::parseSql()等字符串替换方法,它不做标识符转义。
插件初始化阶段关闭模拟预处理
插件独立连接数据库时,若未显式关闭PDO模拟预处理,低版本MySQL或Docker环境会退化为字符串拼接,日志显示Binding但实际未生效。
在插件数据库配置中添加:'params' => [PDO::ATTR_EMULATE_PREPARES => false];该配置必须在插件首次Db::connect()前生效。
验证是否真正生效:执行一次带参数的查询后,调用$db->getRealSql(),若返回WHERE id = 123而非WHERE id = ?,说明模拟预处理仍开启,需立即修正配置。



















