漏洞思维补完:从客户端声明到可信业务事实——一次 HTTP 请求到底是怎样被服务器“理解”的

漏洞思维补完:从客户端声明到可信业务事实——一次 HTTP 请求到底是怎样被服务器“理解”的

前言:我以前把“前端不可信”理解得太简单了

前面写《漏洞思维》的时候,我已经慢慢接受了一个观点:

参数只是业务变量在网络请求里的技术表现形式。

比如:

user_id
order_id
status
price
quantity

它们真正重要的地方,从来不是:

这是一个参数

而是:

它在业务里代表什么?

所以以前我看到:

id=888

会慢慢从:

“这是一个可以修改的数字”

转变成:

“这是一个业务对象定位符”

看到:

status=submitted

也不会只理解成:

“这是一个字符串”

而会继续想:

“它可能在表达一个业务对象当前所处的阶段”

但直到最近真正开始分析靶场数据包,我才发现:

仅仅知道“参数背后是业务变量”还不够。

因为一个请求包里面,并不是所有字段都在做同一件事情。

它可能同时混合着:

身份凭证
用户意图
目标对象
客户端声明
业务事实
状态
关系
范围
派生结果

而服务器也不会用同一种方式处理它们。

于是我开始重新理解:

一条 HTTP 请求,本质上不是客户端把一堆“事实”交给服务器,而是客户端向服务器提交了一组身份凭证、目标选择、行为意图和事实声明。服务器真正要做的,是把这些声明重新验证、解析、计算,最后建立一个自己可以信任的业务世界。

这篇文章,就是把这件事情彻底想清楚。


一、为什么前端明明不可信,还要做校验

以前学安全时,很容易听到一句:

不要相信前端

于是我很自然会产生一个问题:

既然前端不可信,那为什么开发还要在前端做各种校验?为什么不全部交给后端?

后来我发现,这其实混淆了两个完全不同的目标。

例如用户注册时:

密码至少 8 位

前端可以在用户只输入 3 位的时候直接提示:

密码长度不足

这样做的价值是:

用户体验更好
减少无意义请求
减少服务器压力
更快发现输入错误

但攻击者完全可以绕开页面,直接构造:

POST /register

password=123

所以后端仍然必须重新检查:

密码长度是否合法?

于是:

前端校验
=
帮助正常用户遵守规则

后端校验
=
真正维持安全和业务规则

所以真正准确的说法不是:

前端没有意义

而是:

前端可以提前帮助用户遵守规则,但不能成为“用户已经遵守规则”的最终证明。


二、为什么“客户端不可信,服务器可信”也不够准确

继续往下以后,我发现:

客户端不可信
服务器可信

这种说法虽然适合入门,但仍然太粗。

因为真正的“信任”不是:

某个机器整体可信不可信

而是:

对于某一个具体事实,这个来源有没有资格证明它?

例如用户提交:

{
  "quantity": 2
}

客户端是在表达:

“我想买 2 件”

这件事情本来就应该由用户决定。

所以:

quantity=2

作为:

用户意图

客户端就是合理来源。

但如果客户端提交:

{
  "price": 1
}

并且服务器把它理解成:

“商品真实价格就是 1”

问题就完全不同。

因为:

用户有资格决定
“我想买几件”

但用户没有资格决定
“商品真实价格是多少”

所以客户端不是:

什么都不能相信

而是:

对于“我想做什么”
客户端可以是合法来源

对于“世界真实是什么样”
客户端通常不能成为最终权威来源

这让我重新理解了“信任边界”:

信任不是组件属性,而是“某个来源对于某个具体事实拥有多大的证明能力”。


三、Token / Session 也不是“整包可信”的印章

另一个以前很容易混淆的问题是:

用户已经登录了,为什么后续访问订单、文件、项目的时候还需要 Session 或 Token?

原因很简单。

登录成功只能证明:

在登录这个时刻
服务器确认:
这个人是用户 A

但下一条:

GET /orders/888

是一个新的 HTTP 请求。

如果什么身份信息都不带,服务器只知道:

“有人请求订单 888”

却不知道:

这个“有人”是谁

所以 Session / Token 的作用是:

登录成功
↓
建立身份

Session / Token
↓
让后续请求重新绑定到这个身份

例如:

session=ABC123
↓
服务器验证
↓
CurrentUser = A

但这里还有一个非常关键的区别:

Session / Token
只能回答:
“谁在说这些话?”

它不能回答:

“他说的所有内容都是真的。”

例如用户 A 带着完全合法的 Session 提交:

{
  "price": 1,
  "role": "admin",
  "balance": 999999
}

Session 最多证明:

这些声明确实来自用户 A

但它不能证明:

A 真的是管理员
A 的余额真的有 999999
商品真实价格真的为 1

所以我现在特别喜欢一句:

Authenticated Claim ≠ True Claim

也就是:

“我知道是谁说的”,不等于“他说的就是事实”。


四、一条请求里的字段,其实不是同一种东西

假设客户端创建订单:

POST /api/order/create
Cookie: session=ABC123
{
  "product_id": 1001,
  "quantity": 2,
  "price": 1,
  "address_id": 888,
  "coupon_id": 20,
  "total_price": 2,
  "remark": "下午送"
}

以前我可能会看成:

7 个参数

现在我会重新分类:

session
→ 身份凭证 Credential

product_id
→ 目标选择 Target

quantity
→ 用户意图 Intent

price
→ 业务事实 Business Fact

address_id
→ 目标对象 + 对象关系

coupon_id
→ 目标对象 + 状态 + 关系

total_price
→ 派生事实 Derived Fact

remark
→ 用户内容

这里最重要的是:

同一个请求里的不同字段,服务器必须用不同的方式处理。

不能统一理解成:

“客户端传来的值,检查一下对不对”

五、服务器真正做的,不是“逐字段对答案”

这里也是我这次对话里想通的一个关键点。

以前我会想象:

客户端:
price=1

服务器:
数据库里是99

1 != 99
→ 拒绝

这种理解不算错,但还是太机械。

更准确地说:

服务器不是为了证明客户端说的话“对不对”,而是根据客户端表达的意图,重新建立一套可信的业务事实。

例如用户提交:

{
  "product_id": 1001,
  "quantity": 2,
  "price": 1
}

一个正常后端真正应该理解成:

用户 A
想购买
商品 1001
数量 2

然后服务器自己开始重建真实世界:

商品1001存在吗?
↓
查商品数据库

商品现在可售吗?
↓
查商品状态

真实价格是多少?
↓
查定价系统 / 数据库

库存是多少?
↓
查库存

用户A能购买吗?
↓
查资格 / 权限 / 限购规则

买2件合法吗?
↓
检查数量、库存、限购

最终多少钱?
↓
服务端重新计算

于是最终形成:

CurrentUser = A
Product = 1001
Quantity = 2
RealPrice = 99
Stock = 10
Total = 198

这时候,这些才是:

服务器认可的业务事实

六、为什么 quantity=2 可以由用户决定,而 price=1 不行

这个问题一开始让我很困惑。

既然:

客户端不可信

那为什么:

quantity=2

服务器可以接受?

答案是:

“客户端可以提供”不等于“客户端说什么服务器都相信”。

quantity 的业务含义是:

用户想买几件

这本来就是用户意图。

所以客户端有:

表达权

但是服务器仍然要做:

quantity > 0?
库存够不够?
有没有超过限购?

所以:

quantity=2
↓
客户端表达意图
↓
服务器验证意图是否合法
↓
合法以后采用

而 price 的业务含义通常是:

商品真实价格

这不是用户意图,而是业务事实。

所以:

price=1

即使客户端传了,服务器也不应该因此认为:

真实价格 = 1

它应该重新:

product_id=1001
↓
查询定价系统
↓
RealPrice=99

甚至完全忽略客户端提供的 price。


七、客户端其实拥有不同程度的“权力”

想到这里,我觉得可以进一步把一个字段拆成三个问题:

表达权
证明权
裁决权

表达权

谁可以提出这个值?

例如:

quantity=2

用户当然可以说:

“我要买2件”

证明权

谁有资格证明这个事实是真的?

例如:

price=99

真正能够证明它的可能是:

商品数据库
定价系统
促销规则

而不是买家。


裁决权

最后谁决定这个动作能不能发生?

例如:

用户想买2件

最终仍然由服务器判断:

库存够不够?
限购是否允许?
当前状态是否允许购买?

所以服务器保留的是:

最终裁决权

这比简单说:

客户端不可信

要精确得多。


八、一个正常后端大概会怎样处理请求

现在我会把正常服务器处理请求理解成这样:

Client Request
↓
原始声明

HTTP / Schema Parsing
↓
请求能不能正常解析?

Authentication
↓
这是谁?

Target Resolution
↓
你想操作的对象到底是什么?

Authorization
↓
你有没有资格操作这个对象?

Authoritative Lookup
↓
关键业务事实真实是什么?

Business Validation
↓
你的意图是否满足业务规则?

State Validation
↓
当前阶段还允许做这件事吗?

Server-side Calculation
↓
价格、金额、折扣、结果重新计算

Trusted Business State
↓
建立服务器认可的业务事实

Execute / Persist
↓
真正执行业务

实际系统不一定严格按照这个代码顺序实现。

有的逻辑可能在:

Gateway
Controller
Middleware
Service
Repository

不同位置。

但对于安全研究来说,比代码顺序更重要的问题是:

为了正确允许这次业务,服务器最终必须确认哪些事实?


九、目标字段和权限之间是什么关系

还有一类字段特别容易产生误解:

order_id
user_id
address_id
file_id
project_id

这些通常不是:

“事实”

而是在表达:

“我想操作哪个对象”

也就是:

Target

例如:

address_id=888

客户端有资格说:

“我想使用地址888”

但这不能证明:

“地址888属于我”

所以服务器还要:

Session
↓
CurrentUser = A

address_id=888
↓
TargetAddress = 888

查询对象关系
↓
Address888.owner = B

A != B
↓
Deny

所以:

客户端能够选择目标,不等于客户端能够决定自己有权访问目标。

这其实和前面订单 user_id 靶场完全一致。


十、我现在怎么看一个数据包

以前:

看到 id
→ 改

看到 status
→ 改

看到 price
→ 改

看到 user_id
→ 改

现在我希望自己先做翻译。

第一步:这是什么业务动作?

看:

Method
Path
Referer
页面上下文

先回答:

用户到底在做什么?

第二步:谁在做?

看:

Cookie
Session
Authorization
JWT
API Key

问:

服务器靠什么确定当前主体?

第三步:在操作谁 / 什么对象?

看:

id
order_id
user_id
file_id
project_id
tenant_id

问:

这些字段到底定位了哪个业务对象?

然后继续:

当前主体和这个对象是什么关系?

第四步:客户端在表达什么意图?

例如:

quantity
keyword
remark
sort

问:

这个意图允许吗?
有什么业务限制?

第五步:客户端有没有在宣布关键业务事实?

例如:

price
role
owner
balance
paid
status
tenant

问:

谁真正有资格证明这个事实?
服务器有没有重新确认?

第六步:有没有状态、关系和前置条件?

即使请求里没有:

status

服务器也可能必须知道:

订单是否已经付款
申请是否已经提交
验证码是否已经使用

所以还要问:

当前状态允许这个动作吗?
前面的必要步骤真的发生了吗?

第七步:有没有应该由服务器计算的结果

例如:

total_price
discount
tax
final_amount

问:

为什么这个值由客户端提供?
服务器有没有重新计算?

十一、参数只是业务变量的表现形式,还可以继续往下拆

现在回头看前面的那句话:

参数只是业务变量在网络请求里的表现形式。

我觉得还可以继续升级。

应该变成:

HTTP 参数
↓
对应业务变量

业务变量
↓
承担不同角色:

身份
目标
意图
关系
状态
业务事实
范围
派生结果

角色不同
↓
正确处理方式不同
↓
错误方式也不同
↓
产生不同漏洞

例如同样一个:

user_id

在不同接口里可能完全不同。

一种情况下:

user_id
→ Target User

那么我要重点考虑:

Authorization

另一种情况下:

user_id
→ Current Identity

如果服务器直接相信客户端声明:

CurrentUser = user_id

那重点就变成:

Authentication / Trust

所以:

不能靠参数名字判断漏洞,要靠参数在当前业务决策中承担的角色判断。


十二、很多漏洞为什么会出现

经过今天这些讨论,我觉得“服务器过于相信客户端”这句话是对的,但还不够完整。

因为有些漏洞确实是:

客户端声明
↓
直接被当成
服务端事实

例如:

price=1
role=admin
owner_id=A

但也有很多漏洞是:

服务器确实做了校验
↓
但检查条件不完整

例如:

已经登录
✓

但订单是不是他的
没有检查

又或者:

对象属于当前用户
✓

但当前状态是否允许修改
没有检查

再或者并发场景:

两个请求看到的事实都是真的
↓
但同时消费同一个即将失效的旧事实
↓
业务不变量仍然被破坏

所以更完整的理解应该是:

漏洞往往发生在服务器没有完整维护一次业务决策所依赖的身份、对象、关系、状态、事实、范围和时间约束。

“信错客户端”只是其中很重要的一类。


十三、我现在对前端和后端的理解

今天讨论完以后,我觉得可以用一句非常简单的话区分:

客户端负责告诉服务器:
“我想做什么”
“我想操作谁”

服务器负责确认:
“你是谁”
“你有没有资格”
“世界真实是什么样”
“现在允不允许”
“最终结果应该是多少”

再进一步:

Session / Token
→ 你是谁?

客户端参数
→ 你想做什么 / 想操作谁?

Authorization
→ 你有没有资格?

Authoritative Data
→ 世界真实是什么样?

Business Rules
→ 当前条件能不能这么做?

Server-side Calculation
→ 最终结果应该是多少?

这样一来:

前端
后端
Session
Token
权限
信任
业务校验
参数篡改

这些以前分散的概念,终于开始连到了一起。


十四、最终形成的一套数据包分析思维

以后再看到一个数据包,我不希望自己第一时间问:

哪个参数能改?

而应该按下面的思维走:

这是什么业务动作?
↓
谁在做?
↓
身份靠什么证明?
↓
在操作什么对象?
↓
客户端在表达什么意图?
↓
客户端有没有声明关键业务事实?
↓
这些事实真正应该由谁证明?
↓
主体和对象之间是什么关系?
↓
当前状态允许吗?
↓
哪些结果应该由服务器重新计算?
↓
服务器有没有少做一步?

真正值得寻找的不是:

“可修改参数”

而是:

“客户端获得了它本来不应该拥有的证明权或决定权”

或者:

“服务器没有重新确认做出业务决策所需的关键事实”

结语:服务器不是在验证客户端,而是在重建可信世界

今天最大的收获,不是终于知道:

price为什么不能改
quantity为什么能改
Session有什么用

而是慢慢看见了它们背后的同一个模型。

客户端发来的请求只是:

一组声明

这些声明里有:

身份凭证
目标
意图
事实声明
状态声明
派生结果

服务器真正应该做的是:

验证身份
解析目标
检查权限
确认关系
读取权威事实
验证状态
执行规则
重新计算

最后建立:

Trusted Business State

然后才决定:

Allow
or
Deny

所以我现在更愿意这样理解一个安全的后端:

服务器不是“相信客户端,然后检查有没有明显错误”,而是把客户端当成提出请求的一方,再根据服务端掌握的身份、对象关系、业务规则、权威数据和状态,重新构造一个足够可信的业务世界。

而漏洞,往往就发生在:

这个重建过程
少了一步
信错了一步
漏了一条关系
漏了一个状态
或者把客户端声明
错误升级成了服务器事实

这也让我重新理解了最开始那句话:

参数只是业务变量在网络请求里的表现形式。

真正的漏洞思维,不是研究:

参数怎么改

而是研究:

这个业务变量到底代表什么,谁有资格决定它,服务器为了相信它,本来应该做什么,而它实际上有没有做到。

posted @ 2026-09-19 20:35  0xMouise  阅读(10)  评论(0)    收藏  举报