7.1 SQL 注入防护


文档摘要

7.1 SQL 注入防护 安全防线从数据站台的入口讲起:SQL 注入是最老牌、破坏力最直接的攻击——用户输入被拼进查询语句,一段字符串改写了整句 SQL 的语义。本节承接第 4 章的查询构造器与模型:日常查询默认就是安全的,风险藏在少数几个「绕开默认」的写法里。学完你应当能说清参数绑定为什么有效,并能用一份自查清单扫出项目里的残留风险。 攻击成立的一瞬间 先看危险写法长什么样,理解它为什么危险: 一行输入改写语义,这就是注入的本质:数据越权成了指令。危害远不止拖库——取决于数据库权限,攻击者可以 UNION 窃取其他表、堆叠语句写数据、甚至利用文件读写能力落盘。防住它的思路也就一句话:把数据与指令重新分开,数据永远只当数据。

7.1 SQL 注入防护

安全防线从数据站台的入口讲起: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::querywhereRaw,列出全部命中;对旧接口提交 ' OR 1=1 -- 复现全表返回;改造成占位符写法后重放同样载荷,确认返回为空结果;再给列表接口的排序参数做白名单映射。结果示例:攻击载荷在改造后成为普通的搜索词,查询按字面匹配返回空。解读:复现的价值在于记住「参数长什么样不重要,进的是槽还是语法层才重要」。变式:给数据库单独建一个只授本库增删改查权限的应用账号,尝试用它执行删表语句并记录报错——最后一环防线的实际手感,来自亲手撞一次墙。

⚠️ 常见坑:「我用的是 ORM,所以不用考虑注入」是最危险的错觉——ORM 的默认方法安全,但 whereRaworder 透传、动态表名都是 ORM 提供的逃生门。安全的单位是写法,不是框架。

一次注入自查的完整记录样例

给自查一个可照抄的形态。某次对练习项目的自查记录如下:第一步全局搜索 Db::querywhereRaworder( 三个关键词,命中六处;第二步逐条判定——四处是构造器常规查询(参数绑定,安全),一处 whereRaw 拼了排序方向(改白名单映射),一处旧接口字符串拼接(改占位符);第三步对两处风险改写后,用攻击载荷复验;第四步把「三个搜索词加一条判定标准」存进团队自查手册,下次新代码提交前先自扫。整个过程半小时,产出可复用。

自查的价值不在这一次扫出多少,而在判定标准沉淀下来之后,每一次代码评审都能按同一把尺子量——防线的维护成本,大头在「标准统一」,不在「技术难度」。

本节要点回顾

  • 注入的本质:数据越权成指令,一行输入改写整句 SQL 语义。
  • 绑定让数据只当数据:构造器与模型默认绑定,原生查询必须占位符。
  • 标识符靠白名单:排序字段、表名、动态列不能绑定,枚举映射是唯一解。
  • 验证与绑定是纵深:形状关与语法关两道闸,谁也不能单独兜底。
  • 最小权限收尾:应用账号只授所需权限,损害控制是最后一环。

入口防线布完,下一节转向出口:进了库的内容同样可能带着 payload 等着被渲染——XSS 与输入过滤见 7.2。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U