TP5.1默认对where()等链式方法启用PDO预处理防注入,但raw()、动态字段未校验、全局过滤失效等仍会导致漏洞;Db::query()需手动绑定参数;动态元数据必须白名单校验,预处理无法防御二次注入。
TP5.1 默认对 where()、find()、select() 等链式查询方法启用 PDO 预处理 + 参数绑定,只要不手动拼接 SQL 字符串,就天然防基础注入。但真实项目里,90% 的注入漏洞都出在“看起来像安全写法”的地方——比如误用 raw()、动态字段未校验、或全局过滤配置失效。
因为框架只对「参数化结构」做绑定,不对字符串拼接做拦截。下面这些写法看似用了 where(),实则完全绕过预处理:
where('id = ' . input('id')) —— 直接字符串拼接,input('id') 是什么,SQL 就执行什么where('title like "%' . input('q') . '%"') —— 双引号内拼接 + 无占位符,% 和引号都由用户控制where($field . ' = ?', [$value]) —— $field 是动态字段名,PDO 不绑定字段名,只绑定值正确写法是让框架识别操作意图:where('title', 'like', '%' . input('q') . '%') 或 where(['title' => ['like', '%' . input('q') . '%']]),这类结构会触发自动参数绑定。
一旦你写了原生 SQL(比如带子查询、JSON_EXTRACT、窗口函数),Db::query() 就不会自动帮你绑定——它只负责执行,不负责安全。这时候必须显式用问号或命名占位符:
Db::query('SELECT * FROM user WHERE status = ? AND level > ?', [input('status'), input('level')])
Db::query('SELECT * FROM user WHERE name = :name', ['name' => input('name')])
Db::query("SELECT * FROM user WHERE name = '" . input('name') . "'") —— 单引号也救不了你注意:Db::bind() 是可选语法糖,本质和数组绑定等效;但如果你参数多、类型杂(比如含 NULL 或布尔值),用 bind() 显式声明更可控。
TP5.1 的 'default_filter'=>'htmlspecialchars,addslashes,strip_tags' 看似一劳永逸,但它只作用于 input()、param() 等入口函数获取的变量,且顺序和语义容易翻车:
addslashes() 不是防 SQL 注入的正解——它不能处理 Unicode 多字节编码绕过,也不兼容所有数据库连接字符集htmlspecialchars() 是防 XSS 的,对 SQL 完全无效;混在里面反而让人误以为“已防护”真正该做的,是在数据进入 SQL 前一刻才决定怎么处理:参数化优先,实在要拼接就用 mysqli_real_escape_string($connection, $str)(且必须传有效连接资源),而不是依赖入口层“一刀切”过滤。
raw()(TP6)和已废弃的 exp()(TP5)本质是告诉框架:“这段我来保证安全,你别插手”。一旦用了,框架跳过所有转义、绑定、校验,原样插入 SQL:
where('id', 'in', raw('(' . input('ids') . ')')) —— 如果 input('ids') 是 1,2,3); DROP TABLE user; --,直接执行order(raw(input('sort'))) // 输入 "id DESC; SELECT * FROM config" → 语法错误或更糟除非你严格白名单校验了输入(比如 in_array(input('sort'), ['id', 'name', 'created_time'])),否则永远不要把用户输入喂给 raw()。动态表名、字段名、排序方向、分组条件——这些都属于“元数据”,必须走白名单,不能靠过滤或转义。
最常被忽略的一点:预处理防不住“二次注入”。比如你把用户输入存进数据库时没过滤,之后又从库中读出来拼进新 SQL,那第一次入库时的“安全”就毫无意义。防注入不是某个函数的事,而是从输入、存储、再到输出的全链路约束。