「JavaWeb审计盲点」List 集合误区:批量操作下的权限逃逸
渗透测试
本文将深入剖析 List 集合在批量查询、批量更新、批量导出等场景下的典型越权模式。这些风险并非源于明显的代码缺陷,而是藏在"合理的业务逻辑"与"容器特性的误用"之间——属于最难被自动化工具发现、却最易被攻击者利用的 Corner Case。
0x00 前言 ======= 在 JavaWeb 开发中,`List` 是最常用的集合类型之一。与 `Set` 强调唯一性不同,List 的核心特性决定了它的典型使用场景: | 特性 | List | Set | 典型场景差异 | |---|---|---|---| | 有序性 | ✅ 插入顺序稳定 | ❌ 无序或按哈希排序 | 需要保持查询结果原始顺序时必选 List | | 可重复性 | ✅ 允许重复元素 | ❌ 元素唯一 | 批量接收前端传参(如勾选多条记录) | | 索引访问 | ✅ 支持 `get(index)` | ❌ 无索引 | 需要按位置操作或分页处理 | | 批量操作 | `subList()`、`addAll()` 等丰富 API | 相对简单 | 大批量数据的切片、合并、批处理 | 也正是List 的"包容性"(允许重复、有序、动态长度)使其成为批量业务操作的首选容器。例如下面的例子,通过接受前端批量的参数,完成简单的批量删除场景: ```java //接收前端批量参数 @PostMapping("/batchDelete") public Result batchDelete(@RequestBody List<Long> ids) { // 前端勾选 10 条记录,后端直接执行批量删除 recordService.removeByIds(ids); return Result.ok("删除成功"); } // 后续在recordService通过 ORM 批量执行(物理删除/逻辑删除) int affected = recordMapper.deleteBatchIds(ids); ``` 0x01 关于List集合的一些误区 ================== 首先跟Set集合不同,**List集合是允许重复元素的**。 其次Java的List在删除元素时,一般会用list.remove(o)/remove(i)方法,来删除第一个匹配的元素对象或者删除指定索引位置的元素。例如下面的例子: ```java List list = new ArrayList< >;(Arrays.asList("A", "B", "C")); //按索引删除 list.remove(1); // 删除索引1的元素 → ["A", "C"] ``` 按索引删除的话,remove后list的内容应该是\["A","C"\]:  如果按照对象删除的话,remove后List的内容应该是\["B","C"\]: ```php list.remove("A"); ```  这里**经常有个误区是,会认为remove()总能精准剔除所有元素**,从上述例子的现象上看确实是这样的,但是**若List集合中存在重复性元素的话**,remove通常只能只删第一个匹配的Object,例如下面的例子,remove后List的内容应该是\["A","B","C"\]: ```php List list = new ArrayList< >;(Arrays.asList("A", "B","B", "C")); //按对象删除(只删第一个匹配) list.remove("B"); ```  以ArrayList为例,查看remove方法的具体实现,remove()方法主要根据List的长度为次数执行循环,会调用了Object类中的equals方法进行比对,在删除的时候是比较两个对象的地址值是否相同,如果相等则执行删除操作。 从实现逻辑可以看到,当找到对应的元素后会执行break操作,**实际上在操作存在重复元素的List集合时只会删除第一个匹配的元素**:  虽然这个逻辑比较简单,但是在实际开发时,由于没有考虑到重复输入等相关场景,会引入一系列的权限逃逸风险。 0x02 批量业务场景的权限盲区案例 ================== 下面是遇到的一些实际案例。 - 案例一:某运营后台批量发放优惠券(重复用户ID部分绕过库存限制) 某运营后台可批量对员工发放相关的优惠券,正常情况下一种类型的优惠券一个员工只允许领取一次。相关关键代码实现如下: 相关Controller通过接受couponId(优惠券类型)以及List集合下相关的员工id列表,未来保证每个员工只能领取一次,首先调用getReceivedUsers方法获取已领取过对应优惠券的员工id,然后进行遍历,如果传入的userId包含已领取过对应优惠券的员工id,则调用remove方法进行剔除,最后完成优惠券的下发逻辑: ```java public Result batchSendCoupon(@RequestBody List<Long> userIds, Long couponId) { List receivedUsers = couponService.getReceivedUsers(couponId); for (Long userId : receivedUsers) { userIds.remove(userId); } couponService.sendToUsers(userIds); } ``` 具体的service实现,这里主要与数据库进行交互: ```java @Service public class CouponService { @Autowired private UserCouponMapper userCouponMapper; /** * 获取已领取指定优惠券的用户ID列表 */ public List getReceivedUsers(Long couponId) { return userCouponMapper.selectUserIdsByCouponId(couponId); } ...... } ``` 从逻辑上看确实考虑了员工重复领取的情况,但是这里并没有考虑到remove方法只会剔除第一个匹配项的问题: ```java // ❌ 问题代码:意图剔除所有已领取用户,实际上只会删除第一个匹配项! for (Long userId : receivedUsers) { userIds.remove(userId); } ``` 那么在实际利用时,只需要在userIds参数中传入重复的userId,即可突破限制到达重复领取的效果,大致过程如下: ```php 用户请求参数:userIds = [1001, 1002, 1002, 1002, 1003] (1001、1003 是新用户,1002 已领取过) 执行过程: receivedUsers = [1002] remove(1002) → userIds 变为 [1001, 1002, 1002, 1003] (还剩两个 1002!) 最终结果:1002 被发放 2 张券,突破"每人限领 1 张"限制 ``` 具体的修复也比较简单,这里研发最终发券前进行了强制去重,当然也可以使用Set集合进行处理: ```java List validUsers = userIds.stream() .filter(id -\>; !receivedUsers.contains(id)) // 剔除已领取 .distinct() // 强制去重 .collect(Collectors.toList()); // 校验:如果去重前后数量不一致,说明有重复参数,可记录日志预警 if (validUsers.size() != userIds.size()) { log.warn("检测到重复用户ID请求,原始数量:{},去重后:{}", userIds.size(), validUsers.size()); } ``` 考虑到代码层逻辑比较复杂,也可以尝试在数据库层进行处理,即使代码层被绕过,也可以通过数据库唯一索引兜底。 - 案例二:权限审批层级绕过 某审批流对一些敏感的操作,需要多层审批,只有当+1,+2以及相关资源owners审批通过后才能进行操作。整体代码比较复杂,抽取关键代码进行说明。 在实际审批时,会传入对应的审批干系人approverIds以及对应的任务id,在审批过程中会对审批人的权限进行校验,校验通过记录对应的审批记录,最后根据审批人层级判断是否满足审批流的权限层级返回审批是否成功: ```java @PostMapping("/approve") public Result approve(@RequestBody List<Long> approverIds, Long taskId) { Task task = taskService.getById(taskId); for (Long approverId : approverIds) { // 检查该审批人是否有权限 if (!hasApprovePermission(approverId, task)) { return Result.fail("无权限"); } // 记录审批 approvalService.record(approverId, taskId); } // 判断审批层级是否足够 if (approverIds.size() >= task.getRequiredLevel()) { taskService.complete(task); } return Result.ok(); } ``` 这里没有考虑到List集合是允许重复元素的特点,如果approverIds可控,那么可以修改成同一层级的审批人,同样也可以完成对应的审批操作,绕过了系统的权限要求,具体攻击路径如下: ```php 审批人列表:[领导A, 领导A, 领导A] (同一领导重复3次) 实际效果: - 权限校验:领导A有权限,3次都通过 - 层级判断:size=3,满足"需3级审批"要求 - 实际:1个人批了3次,完成本需3人审批的敏感操作 ``` 0x03 一些思考 ========= 这些风险并非源于明显的代码缺陷,而是藏在"合理的业务逻辑"与"容器特性的误用"之间——属于最难被自动化工具发现、却最易被攻击者利用的 Corner Case。 在传统SAST视觉下,对于上面的例子,`List` 在字节码层面是 `List`,SAST 难以追踪 `userIds` 与 `receivedUsers` 的业务关系("已领取 vs 待发放")。同时每人限领1张"规则在注释、需求文档或数据库唯一索引中,并不会在代码中直接体现。 SAST 由于缺乏对应的领域知识图谱,无法将"不规范的 `remove` 调用"与"漏洞绕过"进行有效关联,更无法理解"重复元素"在特定业务语境下的攻击语义。 借助 AI 大模型的上下文理解能力,正是一个值得深入探索的全新检测范式。通过业务需求文档、方法命名、API 路径、数据库表结构等多源信息融合,构建"优惠券发放-用户领取-库存控制"的业务语义网络,利用大模型的推理能力精准识别 `remove` 操作与后续业务校验的逻辑断层,甚至结合历史漏洞案例库举一反三,有望将原本依赖人工审计的 Corner Case 转化为可规模化、可复现的智能检测能力,填补传统 SAST 在业务语义理解层面的关键空白。
发表于 2026-04-09 09:00:01
阅读 ( 1314 )
分类:
代码审计
3 推荐
收藏
0 条评论
tkswifty
67 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!