SQL 注入与 ThinkPHP 漏洞技术讲义
文档定位:安全培训讲义 / 技术分享稿 / 内部整改参考文档
适用对象:PHP 开发人员、安全测试人员、代码审计人员、运维与项目负责人
主题聚焦:SQL 注入原理、ThinkPHP 场景下的触发机制、常见脆弱点、审计方法、修复规范与工程化治理
目录
-
- 文档导读
-
- SQL 注入的本质与形成条件
-
- SQL 注入的常见类型
-
- SQL 注入的危害分层
-
- 为什么 ThinkPHP 项目经常出现相关问题
-
- ThinkPHP 中 SQL 注入的高发位置
-
- 典型脆弱案例拆解
-
- 攻击者通常如何验证 ThinkPHP 注入点
-
- 代码审计时的检查路径
-
- 为什么简单过滤关键字通常无效
-
- ThinkPHP 的防御与修复原则
-
- 从单点漏洞到完整攻击链
-
- 验证、复现与修复回归方法
-
- 开发中的典型误区
-
- 审计人员的实战建议
-
- 老项目整改方案
-
- 可直接复用的安全编码模板
-
- 总结与检查清单
1. 文档导读
很多文章讲 SQL 注入,只停留在"给几个 payload,看几个回显页面"的层面。这种写法适合初学者快速建立印象,但不适合做真正的技术理解,也不适合拿去指导团队整改。
这份文档的目标不是堆概念,而是把三个问题讲透:
- SQL 注入为什么会出现。
- 在 ThinkPHP 项目中,问题通常藏在什么位置。
- 怎样从代码、规范和工程治理三个层面把它真正压下去。
如果你是开发人员,重点看第 5 节到第 11 节。
如果你是安全测试或代码审计人员,重点看第 6 节到第 16 节。
如果你是做内部培训或分享,这份文档也可以直接作为讲稿底稿。
图 1:SQL 注入整体攻击路径图
是
否
用户输入
URL 参数 表单 JSON 请求
应用接收参数
是否安全处理
参数化查询 / 白名单约束
数据库按数据处理输入
业务正常返回
字符串拼接 / 原生表达式 / 动态结构位
SQL 语义被攻击者改变
数据读取
认证绕过
数据篡改
进一步控制系统或后台
2. SQL 注入的本质与形成条件
2.1 SQL 注入到底是什么
SQL 注入的本质,不是"数据库被攻破"这么简单,而是:
应用程序把本应作为数据处理的外部输入,错误地带入了 SQL 语句的语法结构中。
一旦这个边界被打穿,攻击者就不再只是提交参数,而是在参与构造 SQL。
换句话说,真正的问题不是某个特殊字符串,而是"数据"和"代码"的边界失守了。
2.2 一个最简单的错误示例
php
$id = $_GET['id'];
$sql = "SELECT * FROM user WHERE id = $id";
正常请求:
text
id=1
SQL 为:
sql
SELECT * FROM user WHERE id = 1
恶意请求:
text
id=1 OR 1=1
SQL 变为:
sql
SELECT * FROM user WHERE id = 1 OR 1=1
此时查询条件被改写,原本只该查一条记录的语句,可能变成整表返回。
2.3 SQL 注入形成的三个必要条件
SQL 注入通常同时满足以下三个条件:
- 应用接收了外部可控输入。
- 输入在进入 SQL 前没有被正确约束。
- 数据库把输入当成了 SQL 语法的一部分执行。
只要这三步连起来,注入就有成立空间。
2.4 一句必须记住的话
SQL 注入不是 payload 的问题,而是输入边界管理失败的问题。
这句话很关键。真正专业的防御,不是记住多少攻击语句,而是确保所有输入都无法越过边界,进入 SQL 结构位。
图 2:参数化查询与字符串拼接对比图
危险路径
用户输入: 1 OR 1=1
直接拼接进 SQL
数据库将其视为语法片段
查询结构被改写
安全路径
用户输入: 1 OR 1=1
参数绑定
数据库将其视为普通值
查询结构不变
3. SQL 注入的常见类型
理解不同类型,不是为了"打得更花",而是为了知道问题可能以什么形式暴露。
3.1 联合查询注入
通过 UNION SELECT 将攻击者构造的查询结果拼接到正常查询结果中。
典型特点:
- 页面会直接显示查询结果。
- 原查询列数、字段类型需要大致匹配。
- 常见于列表页、详情页、搜索页。
3.2 报错注入
通过触发数据库错误,把目标数据夹带进错误信息中返回。
典型特点:
- 页面或接口会返回数据库报错。
- 开发环境、测试环境或日志泄露场景更常见。
3.3 布尔盲注
页面不直接返回数据,攻击者就利用"条件成立"和"条件不成立"时页面反馈的差异,一位一位猜解信息。
典型特点:
- 页面有真假差异。
- 不需要明显报错。
- 获取数据较慢,但稳定性往往更高。
3.4 时间盲注
页面既不报错,也没有明显内容差异时,攻击者会借助数据库延时函数,通过响应时间变化判断条件是否成立。
典型特点:
- 接口响应时间可观测。
- 常用于深藏较深的接口或 JSON API。
3.5 堆叠注入
某些场景下,如果驱动或执行方式允许一次执行多条 SQL,攻击者可能在原语句后继续追加新语句。
这种方式能不能成立,取决于:
- 数据库类型
- 驱动配置
- 框架执行方式
- 是否允许多语句执行
因此它不是所有场景都能用,但一旦成立,破坏力通常更大。
图 3:SQL 注入类型对照图
SQL 注入类型
联合查询注入
报错注入
布尔盲注
时间盲注
堆叠注入
页面可回显查询结果
数据库错误信息可见
页面存在真假差异
可通过延迟判断条件
驱动或配置允许多语句执行
| 类型 | 页面特征 | 典型前提 | 利用成本 | 风险说明 |
|---|---|---|---|---|
| 联合查询注入 | 有回显 | 列数与类型可适配 | 中 | 读取数据效率高 |
| 报错注入 | 有报错 | 错误信息暴露 | 低到中 | 调试环境中常见 |
| 布尔盲注 | 有真假差异 | 页面逻辑可观察 | 高 | 稳定但较慢 |
| 时间盲注 | 仅时间差异 | 接口响应时间可测 | 高 | 无回显时常用 |
| 堆叠注入 | 取决于执行环境 | 支持多语句 | 高 | 成立后破坏力强 |
4. SQL 注入的危害分层
谈风险时,不能只说一句"可能泄露数据库"。这太粗糙。实际危害往往分层推进。
4.1 第一层:数据读取
最基础、最直接的后果包括:
- 读取用户表、管理员表
- 读取手机号、邮箱、身份证等敏感信息
- 读取订单、支付、财务数据
- 读取配置表、业务规则表、权限表
4.2 第二层:认证绕过
如果登录逻辑写得不严谨,攻击者可能通过构造条件绕过口令校验,直接进入后台。
4.3 第三层:数据篡改
在数据库具备写权限的情况下,攻击者可能:
- 修改管理员密码
- 修改账号角色
- 删除业务数据
- 注入后门配置
4.4 第四层:文件与系统层扩展
在高权限数据库配置下,攻击者可能进一步尝试:
- 读取服务器敏感文件
- 写出恶意文件
- 获取 WebShell 落点
- 扩展到系统控制层
4.5 第五层:横向扩展与业务接管
数据库里往往不止业务数据,还可能藏着:
- 邮件服务凭证
- 短信平台密钥
- 云存储配置
- 支付回调密钥
- 第三方接口令牌
一旦这些信息被读到,攻击链就会迅速跨出数据库本身。
讲义提示:
做培训时,可以在这一节强调一句:
"SQL 注入常常不是终点,而是后续接管的起点。"
5. 为什么 ThinkPHP 项目经常出现相关问题
这里需要先澄清一个容易被误读的点:
ThinkPHP 不等于 SQL 注入。真正的问题往往是框架能力被误用,或者老版本项目存在历史包袱。
5.1 使用量大,老项目多
ThinkPHP 在国内项目中长期非常常见,尤其是中小型业务系统、后台管理系统、行业项目、定制项目。
这意味着:
- 历史版本多
- 维护水平参差不齐
- 安全标准不统一
- 代码风格差异大
项目越多、版本越杂、开发质量越不均衡,安全问题就越容易集中暴露。
5.2 查询构造灵活,误用空间也大
ThinkPHP 支持:
- 链式查询
- 数组条件
- 动态字段
- 表达式查询
- 原生 SQL
这些能力本身没有问题,但只要开发者把外部输入直接带进去,风险会非常快地放大。
5.3 老项目常见"赶工式"代码
很多历史项目里最常见的不是复杂漏洞,而是这种为了快速上线的写法:
php
$map = input('get.');
$list = Db::name('user')->where($map)->select();
或者:
php
$order = input('get.order');
$list = Db::name('user')->order($order)->select();
又或者:
php
$sql = "select * from user where id = " . input('get.id');
$res = Db::query($sql);
这些代码的问题不在于"不优雅",而在于直接失去了输入边界。
5.4 安全治理通常落后于业务迭代
ThinkPHP 项目很多时候不是没人知道有风险,而是:
- 业务先跑起来
- 安全后补
- 老模块没人敢动
- 接口越来越多
- 开发规范没统一
于是本来只是一个局部问题,最后会积累成系统性风险。
6. ThinkPHP 中 SQL 注入的高发位置
这一节是整篇文档的核心。真正做审计或整改,重点就是找这些位置。
6.1 原生 SQL 查询拼接
最直接、最危险、最不该出现在核心业务里的写法:
php
$id = input('get.id');
$res = Db::query("SELECT * FROM tp_user WHERE id = $id");
或者:
php
$username = input('post.username');
$res = Db::query("SELECT * FROM tp_user WHERE username = '$username'");
风险原因很简单:
框架的参数绑定和查询构造器保护机制被完全绕开了。
高风险接口
Db::query()Db::execute()- 模型中的自定义原生 SQL
- 任何把用户输入直接拼进 SQL 字符串的代码
6.2 where() 被整包参数直接驱动
例如:
php
$params = input('get.');
$list = Db::name('user')->where($params)->select();
或者:
php
$where = input('post.where');
$list = Db::name('user')->where($where)->select();
问题在于,开发者以为自己是在"偷懒做通用查询",实际上是在把条件构造能力开放给外部输入。
6.3 排序注入:order() 是非常高频的薄弱点
后台列表页里最常见的问题往往不是 where,而是 order:
php
$order = input('get.order');
$list = Db::name('user')->order($order)->select();
这类代码出现频率非常高,因为排序功能看起来不像"危险点",开发者防范意识普遍偏弱。
6.4 字段、表名、联表等结构位可控
比如:
php
$field = input('get.field');
$data = Db::name('user')->field($field)->select();
或者:
php
$table = input('get.table');
$data = Db::table($table)->select();
只要让外部输入控制了 SQL 结构位,问题就已经进入高危区域。
6.5 表达式查询与原生片段
例如:
php
Db::name('user')->where('id', 'exp', "= $id")->select();
或者:
php
$exp = input('get.exp');
Db::name('user')->where('status', 'exp', $exp)->select();
exp 的含义本质上就是"按表达式插入,而不是按普通值处理"。
这种接口只适合固定表达式,不适合任何外部输入。
6.6 raw 类接口
例如:
whereRaw()orderRaw()fieldRaw()havingRaw()
这些接口只要和外部输入发生直接拼接,就应视为高风险。
图 4:ThinkPHP 高危查询接口地图
ThinkPHP 查询相关接口
原生 SQL
结构位控制
表达式与 Raw
动态条件组装
Db::query
Db::execute
order
field
table
join
group
limit
whereRaw
orderRaw
fieldRaw
havingRaw
exp
where(input get.)
where(input post.)
动态 map 条件
| 风险等级 | 典型接口 | 主要问题 |
|---|---|---|
| 极高 | Db::query() Db::execute() |
直接手工拼接 SQL |
| 高 | whereRaw() orderRaw() fieldRaw() exp |
原生表达式直接进入执行链 |
| 高 | order() field() table() join() |
外部输入控制 SQL 结构位 |
| 中高 | where($map) where(input('get.')) |
条件构造权被交给前端参数 |
7. 典型脆弱案例拆解
下面给一个非常接近真实业务的例子:
php
public function index()
{
$keyword = input('get.keyword');
$order = input('get.order');
$sql = "SELECT id,username,email FROM tp_user
WHERE username LIKE '%$keyword%'
ORDER BY $order";
$list = Db::query($sql);
return json($list);
}
7.1 这段代码的问题在哪里
它实际上有两个独立的注入点:
keyword进入了LIKE子句。order进入了ORDER BY结构位。
7.2 为什么这种代码在项目里很常见
因为它完全符合"快速做一个列表接口"的思路:
- 支持搜索
- 支持排序
- 一条 SQL 搞定
- 上线很快
但这类代码的代价,就是把输入边界交了出去。
7.3 正确重构思路
安全改法不是"多过滤几个字符",而是拆分两类输入:
- 值参数:走参数化或查询构造器
- 结构参数:走白名单
可改成:
php
public function index()
{
$keyword = input('get.keyword', '', 'trim');
$field = input('get.field', 'id', 'trim');
$sort = input('get.sort', 'desc', 'trim');
$allowFields = ['id', 'username', 'create_time'];
if (!in_array($field, $allowFields, true)) {
$field = 'id';
}
$sort = strtolower($sort) === 'asc' ? 'asc' : 'desc';
$list = Db::name('user')
->field('id,username,email')
->whereLike('username', "%{$keyword}%")
->order($field . ' ' . $sort)
->select();
return json($list);
}
7.4 这个重构为什么有效
因为修复点不在于"替换了几个函数",而在于边界重新建立了:
keyword只作为值使用- 排序字段来自白名单
- 排序方向被限制为固定枚举
- 整个查询不再依赖原生 SQL 拼接
图 5:脆弱代码与安全代码对照图
脆弱实现
原生 SQL 拼接
keyword 直接进入 LIKE
order 直接进入 ORDER BY
用户输入影响 SQL 结构
安全实现
查询构造器
keyword 作为普通值
field 与 sort 白名单
用户输入无法改写 SQL 结构
text
脆弱实现:
用户输入 -> 拼接 SQL -> 数据库按语法执行 -> 可能注入
安全实现:
用户输入 -> 类型约束 / 白名单 / 参数化 -> 数据库按数据处理 -> 查询结构稳定
8. 攻击者通常如何验证 ThinkPHP 注入点
从防守视角理解攻击路径,有助于知道自己该优先检查什么。
8.1 先看参数名
以下参数名经常值得重点关注:
idkeywordsearchsortorderfieldfilterwheremapids
它们不一定都有漏洞,但它们常常代表"开发者想做动态查询"。
8.2 再看反馈方式
攻击者通常会观察:
- 是否出现 SQL 报错
- 页面内容是否有真假差异
- JSON 结构是否异常
- 响应时间是否明显变化
8.3 识别 ThinkPHP 项目特征
一旦判断目标是 ThinkPHP 项目,攻击者往往会更有针对性地测试:
- 动态查询条件
- 排序字段
raw接口误用exp表达式误用- 历史版本遗留问题
8.4 后台管理功能尤其容易出问题
风险高发区域包括:
- 列表页
- 搜索页
- 导出页
- 报表页
- 批量操作接口
因为这些地方天然依赖筛选、排序、统计、联表,最容易出现拼接式代码。
9. 代码审计时的检查路径
如果你在审计 ThinkPHP 项目,建议先做全局检索。
9.1 建议优先搜索的关键字
text
Db::query(
Db::execute(
whereRaw(
orderRaw(
fieldRaw(
havingRaw(
exp
order(
field(
table(
join(
input(
$_GET
$_POST
$_REQUEST
9.2 审计时不要只看"这一行"
应按数据流来判断:
- 参数从哪里来。
- 是否可被用户直接控制。
- 是否经过类型约束。
- 是否经过白名单限制。
- 最终进入的是值位还是结构位。
- 是否被作为原生 SQL 或表达式片段处理。
9.3 几类典型高风险模式
php
Db::query("... $var ...")
Db::execute("... {$var} ...")
->order(input('get.order'))
->field(input('get.field'))
->where(input('post.'))
->where($map)
->where('id', 'exp', $exp)
只要出现"外部输入 -> SQL 结构位 / 原生表达式"的路径,就应该继续深挖。
9.4 发现一个点,不要只修一个点
很多项目的漏洞不是孤立的,而是编码习惯导致的成片问题。
如果一个控制器里已经出现动态 order(),同风格代码里通常还会有动态 field()、动态 where()、原生 SQL 拼接等问题。
10. 为什么简单过滤关键字通常无效
很多老项目喜欢这样处理:
php
$id = str_replace(['select', 'union', "'"], '', input('get.id'));
这种方式看起来像做了防护,实际非常脆弱。
10.1 黑名单永远补不完
你今天封了 union,明天还有大小写绕过、注释绕过、编码绕过、函数替代、逻辑替代。
10.2 注入不只依赖几个关键字
很多注入方式并不需要典型关键字,也不一定依赖引号闭合。
10.3 容易误伤正常输入
粗暴删字符常常会导致业务参数本身被污染。
10.4 结构位问题根本不是过滤能解决的
例如排序字段,正确做法不是"过滤危险字符",而是:
字段只能来自预定义白名单。
结论很明确:
- 值参数靠参数化
- 结构参数靠白名单
- 不要迷信黑名单过滤
11. ThinkPHP 的防御与修复原则
这一节适合直接写进团队编码规范。
11.1 能不用原生 SQL 就不要拼原生 SQL
错误写法:
php
Db::query("SELECT * FROM user WHERE id = $id");
推荐写法:
php
Db::name('user')->where('id', (int)$id)->find();
11.2 值参数必须被当作值处理
比如:
- 用户名
- 关键字
- 手机号
- 状态值
- 时间值
这些都应该进入参数化查询或查询构造器,而不是拼进字符串。
11.3 结构参数必须白名单
以下都属于结构参数:
- 排序字段
- 排序方向
- 字段列表
- 分组字段
- 表名
- 联表片段
这些参数不能靠"过滤"处理,只能从固定枚举中选。
11.4 禁止整包请求直接进 where
错误方式:
php
$params = input('get.');
Db::name('user')->where($params)->select();
正确方式:按字段逐个接收、逐个校验、逐个构造。
11.5 exp、raw、自定义 SQL 视为高敏感能力
这些接口不是绝对禁止,但必须满足:
- 表达式由开发者内部固定生成。
- 不接受任何未经白名单约束的外部输入。
11.6 最小化数据库权限
即使发生注入,也应尽量缩小损害面。
业务库账号不应具备超出实际需要的高危权限。
图 6:错误防护思路与正确防护思路对照图
正确思路
值参数参数化
结构参数白名单
禁止外部输入进入 raw / exp
数据库最小权限
修复后做安全回归
错误思路
过滤几个危险字符
黑名单拦截关键字
只盯单引号
认为后台接口没人碰
12. 从单点漏洞到完整攻击链
SQL 注入最容易被低估的地方,就是很多人以为它只是"多查了几条数据"。
现实中,一条注入链很可能这样发展:
- 读取管理员表。
- 获取口令哈希、会话标识或重置令牌。
- 接管后台。
- 借助文件上传、插件管理、模板编辑等能力落地后门。
- 进一步控制服务器。
另一条常见路径是:
- 读取配置表。
- 获取邮件、短信、对象存储或第三方 API 密钥。
- 接管外围服务。
- 扩大为业务级事故。
因此整改不能只盯着"这条 SQL 改没改",还要看:
- 数据库权限是否过高
- 后台是否存在弱口令
- 上传链路是否安全
- 配置文件是否已泄露
- 日志里是否已有攻击痕迹
13. 验证、复现与修复回归方法
这里强调一点:
复现的目标是验证风险与修复效果,而不是做破坏性展示。
13.1 验证思路
建议按以下步骤进行:
- 确认参数进入 SQL 的位置。
- 判断它是值位还是结构位。
- 用最轻量的方式验证是否能引起语义变化。
- 观察报错、真假差异、时间差异或结果异常。
- 修复后做同路径回归。
13.2 测试中的边界要求
- 优先在测试环境或镜像环境进行。
- 不要直接对生产做破坏性验证。
- 记录测试前后的响应差异。
- 不为了"证明漏洞"而做越界行为。
13.3 修复成功的判定标准
修复不能只看"原来的 payload 不生效了",还要看:
- 用户输入是否只能作为数据出现
- 结构参数是否被白名单锁死
- 原生表达式接口是否已与外部输入隔离
- 正常业务功能是否仍然可用
14. 开发中的典型误区
14.1 误区一:用了框架就天然安全
错。框架提供的是能力,不是免责。
14.2 误区二:只要过滤单引号就行
错。SQL 注入远不止靠引号闭合。
14.3 误区三:数字参数不会注入
错。只要数字没有被强制约束,照样可能进入表达式逻辑。
14.4 误区四:后台接口没人知道,不会被打
错。后台列表、导出、筛选接口恰恰是高发区域。
14.5 误区五:不报错就说明安全
错。还有布尔盲注和时间盲注。
15. 审计人员的实战建议
15.1 审计优先级建议
第一优先级:
Db::query()Db::execute()raw类接口exp表达式
第二优先级:
- 后台列表页
- 搜索接口
- 导出接口
- 统计报表接口
第三优先级:
- 动态排序
- 动态字段
- 动态时间范围
- 批量 ID 参数
- 通用筛选 map
15.2 审计时关注"模式",不要只盯"单点"
如果某个模块里已经出现:
php
->order(input('get.order'))
那就不要只修这一行,而要继续检查整个项目是否普遍存在同类写法。
真正高效的审计,抓的是编码习惯。
16. 老项目整改方案
如果你接手的是历史较久的 ThinkPHP 项目,建议按这个顺序推进。
16.1 第一步:全局盘点
先把所有涉及以下能力的代码搜出来:
- 原生 SQL
- 动态排序
- 动态字段
- 表达式查询
- 整包参数进
where
16.2 第二步:按风险分级
高危:
- 原生 SQL 拼接
exp接外部输入raw接外部输入
中危:
- 动态
order - 动态
field - 动态
table
一般风险:
- 缺少类型约束
- 缺少参数枚举
16.3 第三步:做统一封装
例如后台列表查询统一约束:
- 允许哪些筛选字段
- 允许哪些排序字段
- 允许哪些排序方向
- 搜索统一走构造器
这样可以从机制上减少"每个控制器自己拼"的情况。
16.4 第四步:把规则纳入代码评审
建议加入 CR 检查项:
- 禁止拼接 SQL 字符串
- 禁止动态表名
- 禁止外部输入直接进入
raw/exp - 禁止整包请求参数直接进入查询条件
16.5 第五步:修复后做回归测试
尤其要验证:
- 搜索是否正常
- 排序是否正常
- 分页是否正常
- 导出是否正常
安全整改不能以牺牲业务稳定性为代价。
17. 可直接复用的安全编码模板
下面给一个更接近实际项目的示例:
php
public function userList()
{
$keyword = input('get.keyword', '', 'trim');
$status = input('get.status/d', -1);
$field = input('get.field', 'id', 'trim');
$sort = input('get.sort', 'desc', 'trim');
$allowOrderFields = ['id', 'username', 'create_time'];
if (!in_array($field, $allowOrderFields, true)) {
$field = 'id';
}
$sort = strtolower($sort) === 'asc' ? 'asc' : 'desc';
$query = Db::name('user')->field('id,username,status,create_time');
if ($keyword !== '') {
$query->whereLike('username', "%{$keyword}%");
}
if ($status >= 0) {
$query->where('status', $status);
}
$list = $query->order($field . ' ' . $sort)->paginate(20);
return json($list);
}
这个模板的核心原则只有三条:
- 输入先分类型处理。
- 值参数走查询构造器或参数化。
- 结构参数走白名单。
如果团队统一坚持这三条,ThinkPHP 项目里的 SQL 注入风险会下降得非常明显。
18. 总结与检查清单
把整篇文档收束成一句话:
ThinkPHP 中绝大多数 SQL 注入问题,并不是框架天然不安全,而是开发者把外部输入带进了 SQL 结构位或原生表达式中。
真正该记住的,不是某条 payload,而是这四条原则:
- 用户输入默认不可信。
- 值和结构必须分开处理。
- 原生 SQL、
raw、exp等能力要高敏感使用。 - 白名单和参数化永远比黑名单可靠。
18.1 自查清单
text
[ ] 是否存在 Db::query / Db::execute 手工拼接 SQL
[ ] 是否存在 whereRaw / orderRaw / fieldRaw 接收外部输入
[ ] 是否存在 exp 表达式接收外部输入
[ ] 是否存在 order() 动态接收前端字段
[ ] 是否存在 field() / table() / join() 动态可控
[ ] 是否存在 input('get.') / input('post.') 直接进入 where
[ ] 是否对排序字段、排序方向做了白名单
[ ] 是否对 ID、状态、分页等参数做了类型约束
[ ] 数据库账号是否采用最小权限
[ ] 修复后是否做了功能回归与安全回归
18.2 适合作为结尾强调的一句话
很多人学 SQL 注入,学的是"怎么打";真正成熟的团队,学的是"为什么这个缺陷会出现,以及怎样在工程上不让它再出现"。
ThinkPHP 恰好是一个很典型的教学样本,因为它既有足够广泛的真实项目基础,也有足够典型的误用场景,特别适合用来讲清安全边界、编码规范和治理思路。