7.1 SQL 注入防护 安全防线从数据站台的入口讲起:SQL 注入是最老牌、破坏力最直接的攻击——用户输入被拼进查询语句,一段字符串改写了整句 SQL 的语义。本节承接第 4 章的查询构造器与模型:日常查询默认就是安全的,风险藏在少数几个「绕开默认」的写法里。学完你应当能说清参数绑定为什么有效,并能用一份自查清单扫出项目里的残留风险。 攻击成立的一瞬间 先看危险写法长什么样,理解它为什么危险: 一行输入改写语义,这就是注入的本质:数据越权成了指令。危害远不止拖库——取决于数据库权限,攻击者可以 UNION 窃取其他表、堆叠语句写数据、甚至利用文件读写能力落盘。防住它的思路也就一句话:把数据与指令重新分开,数据永远只当数据。
安全防线从数据站台的入口讲起:SQL 注入是最老牌、破坏力最直接的攻击——用户输入被拼进查询语句,一段字符串改写了整句 SQL 的语义。本节承接第 4 章的查询构造器与模型:日常查询默认就是安全的,风险藏在少数几个「绕开默认」的写法里。学完你应当能说清参数绑定为什么有效,并能用一份自查清单扫出项目里的残留风险。
先看危险写法长什么样,理解它为什么危险:
// 危险:用户输入直接拼进 SQL $keyword = $request->param('keyword'); $list = Db::query("SELECT * FROM book WHERE title LIKE '%" . $keyword . "%'"); // 提交 keyword = ' OR 1=1 -- 时,语句变成: // SELECT * FROM book WHERE title LIKE '%' OR 1=1 -- %' // OR 1=1 让全表命中,-- 注释掉收尾引号,条件彻底失效
一行输入改写语义,这就是注入的本质:数据越权成了指令。危害远不止拖库——取决于数据库权限,攻击者可以 UNION 窃取其他表、堆叠语句写数据、甚至利用文件读写能力落盘。防住它的思路也就一句话:把数据与指令重新分开,数据永远只当数据。
绑定的原理是「语句骨架先交给数据库编译,数据后到、只进参数槽」——槽里的任何字符都不会再被解析成 SQL 语法,引号、百分号、注释符全部退化为普通字符:
// 安全写法一:查询构造器,参数绑定是默认行为 $list = Db::name('book') ->whereLike('title', '%' . $keyword . '%') ->select(); // 安全写法二:原生查询用占位符,禁止字符串拼接 $list = Db::query('SELECT * FROM book WHERE title LIKE ? LIMIT ?', ['%' . $keyword . '%', 20]); // 安全写法三:模型查询,同样默认绑定 $list = Book::where('title', 'like', '%' . $keyword . '%')->select();
注意第三例里 $keyword 虽然拼在 like 的模式串里,但它整个是一个参数值,绑定层保证它不会被拆解——模式串里允许通配符是业务语义(模糊搜索),与语法注入是两回事。若要防用户输入通配符导致的慢查询,做的是转义百分号这类业务处理,与安全无关。
绑定是默认,但有几处写法会主动绕开它,全部值得在项目里全局搜索一遍:
| 风险点 | 典型写法 | 风险与处置 |
|---|---|---|
| 原生表达式拼接 | whereRaw('price > ' . $input) | 输入直通语法层。改占位参数 whereRaw('price > ?', [$input]) |
|
| 排序字段透传 | order($request->param('sort')) |
字段名不能绑定。用白名单映射:仅允许命中预设的排序键 |
| 动态表名或字段 | Db::name($input)->... |
表名不能绑定。白名单校验后引用,或彻底改为枚举映射 |
这三处的共同点是「标识符(表名、字段名、排序键)没法当参数」,唯一解法是白名单:把可取值枚举在代码里,输入只是从枚举里挑一个下标。排序列是最高频的踩坑点——前端传 sort=price desc 看似无害,一旦透传给 order,攻击者可以借此探测或构造子查询,白名单映射('price' => 'price'、'newest' => 'id desc')成本极低,建议所有列表接口标配。
参数绑定挡住语法注入后,入口还有两层在协同:6.1 的验证器先核形状(book_id 必须是正整数,攻击串连形状关都过不去),绑定再保证侥幸过关的字符串只当数据。两层叠加后,注入在框架层的生存空间已极小,剩下的功夫花在权限与损害控制上:数据库账号按环境分权,应用连接只用增删改查所需的最小权限——即使全部防线失守,DROP 与写文件的能力也不在应用账号手里。这一层不归框架管,归部署(第 8 章上线检查单有一席),但它是纵深防御的最后一环,提前在这里点名。
背景:练习项目历史代码里留有一处原生拼接查询(图书搜索的旧接口)。操作:全局搜索 Db::query 与 whereRaw,列出全部命中;对旧接口提交 ' OR 1=1 -- 复现全表返回;改造成占位符写法后重放同样载荷,确认返回为空结果;再给列表接口的排序参数做白名单映射。结果示例:攻击载荷在改造后成为普通的搜索词,查询按字面匹配返回空。解读:复现的价值在于记住「参数长什么样不重要,进的是槽还是语法层才重要」。变式:给数据库单独建一个只授本库增删改查权限的应用账号,尝试用它执行删表语句并记录报错——最后一环防线的实际手感,来自亲手撞一次墙。
⚠️ 常见坑:「我用的是 ORM,所以不用考虑注入」是最危险的错觉——ORM 的默认方法安全,但
whereRaw、order透传、动态表名都是 ORM 提供的逃生门。安全的单位是写法,不是框架。
给自查一个可照抄的形态。某次对练习项目的自查记录如下:第一步全局搜索 Db::query、whereRaw、order( 三个关键词,命中六处;第二步逐条判定——四处是构造器常规查询(参数绑定,安全),一处 whereRaw 拼了排序方向(改白名单映射),一处旧接口字符串拼接(改占位符);第三步对两处风险改写后,用攻击载荷复验;第四步把「三个搜索词加一条判定标准」存进团队自查手册,下次新代码提交前先自扫。整个过程半小时,产出可复用。
自查的价值不在这一次扫出多少,而在判定标准沉淀下来之后,每一次代码评审都能按同一把尺子量——防线的维护成本,大头在「标准统一」,不在「技术难度」。
入口防线布完,下一节转向出口:进了库的内容同样可能带着 payload 等着被渲染——XSS 与输入过滤见 7.2。