跳转到帖子

背景在日常代码审计里,越权问题一直是比较容易被低估的一类漏洞。它不像命令执行、反序列化那样“显眼”,但在业务系统中影响往往很直接:普通用户可以查看或修改其他用户的数据,运营角色可以触达管理员接口,或者租户之间的数据边界被打穿。这篇文章整理一次比较典型的 Spring Boot 接口越权排查过程,重点放在审计思路、复现验证和修复建议上。文中涉及的接口和字段都做了简化处理,适合用于内部安全评审、研发自查和测试环境验证,不涉及真实系统攻击。问题场景某业务系统采用前后端分离架构,后端是 Spring Boot,认证方式为 JWT。用户登录后,前端会在请求头中携带 token,后端通过拦截器解析用户身份。审计时发现一个订单详情接口,表面上已经要求登录,但没有进一步校验订单归属关系:GET /api/order/detail?id=10086
Authorization: Bearer eyJhbGciOi...接口返回订单基础信息、收货信息、支付状态等敏感数据。如果后端只根据 id 查询订单,而不判断该订单是否属于当前登录用户,就可能形成水平越权。代码结构简化项目里常见的分层结构如下:controller
└── OrderController.java

service
└── OrderService.java

mapper
└── OrderMapper.java

security
├── JwtInterceptor.java
└── LoginUserContext.java在 Controller 层看到的代码大致如下:@GetMapping(\"/detail\")
public Result<OrderDetailVO> detail(@RequestParam Long id) {
OrderDetailVO detail = orderService.getDetail(id);
return Result.ok(detail);
}Service 层代码:public OrderDetailVO getDetail(Long id) {
Order order = orderMapper.selectById(id);
if (order == null) {
throw new BizException(\"订单不存在\");
}

return buildOrderDetail(order);
}这个实现的问题在于:接口确认了“订单是否存在”,但没有确认“当前用户是否有权限访问该订单”。认证和授权被混在一起理解,是这类问题最常见的根因。技术分析判断一个接口是否存在越权,不能只看有没有 token 校验。登录态只能说明请求者是谁,不能说明请求者能访问什么。对这类接口,我通常会从以下几个点入手:接口是否使用了用户可控的资源标识,例如 id、userId、tenantId、orderNo。后端查询时是否把当前登录用户信息纳入条件。是否存在前端隐藏字段控制权限的情况,例如 role、ownerId 从请求参数传入。是否存在批量接口,例如 /list、/export、/batchDetail,它们更容易遗漏权限过滤。管理员、运营、普通用户等角色是否共用同一套 Service 方法。以上案例中,订单 id 来自请求参数,后端直接通过主键查询,风险点比较明确。安全复现思路复现越权问题时,建议只在授权测试环境中进行,并使用测试账号和测试数据。目标不是“猜测更多 id”,而是证明权限边界是否缺失。一个比较稳妥的验证流程如下:创建两个测试用户:userA 和 userB。分别登录,获取各自 token。使用 userA 创建一条测试订单,记录订单 id。使用 userB 的 token 请求 userA 的订单详情。观察接口是否返回完整订单信息。示例请求如下:curl -i \\
-H \"Authorization: Bearer ${USER_B_TOKEN}\" \\
\" 200 且包含 userA 的订单数据,基本可以确认存在水平越权。如果返回 403、404,或返回空数据,则需要继续查看是否只是某个字段被过滤,还是权限控制完整生效。容易误判的情况越权验证时有几个点需要注意,否则容易误报或漏报。有些系统会对订单号做脱敏展示,但底层仍返回完整地址、手机号、金额等敏感字段,要看完整响应体。有些接口通过网关做权限控制,本地代码看不到鉴权逻辑,需要结合网关配置一起确认。有些业务允许客服、运营访问用户订单,这属于垂直授权,需要看角色边界和审计日志。不要只测详情接口,列表、导出、状态修改、备注修改等接口也要一起看。修复建议修复水平越权问题的核心原则是:资源查询必须绑定当前主体,不能仅依赖客户端传入的资源标识。较直接的修复方式是在查询条件中加入当前登录用户 id:public OrderDetailVO getDetail(Long id) {
Long currentUserId = LoginUserContext.getUserId();

Order order = orderMapper.selectByIdAndUserId(id, currentUserId);
if (order == null) {
throw new BizException(\"订单不存在或无权访问\");
}

return buildOrderDetail(order);
}对应 SQL 逻辑:SELECT *
FROM t_order
WHERE id = #{id}
AND user_id = #{userId}
AND deleted = 0
LIMIT 1;这里建议不要先 selectById 再在 Java 里判断 userId。原因有两个:更容易遗漏判断,后续维护时可能直接复用存在风险的方法。从数据访问层就限制结果范围,安全边界更清晰。如果是多租户系统,还需要绑定 tenantId:SELECT *
FROM t_order
WHERE id = #{id}
AND tenant_id = #{tenantId}
AND user_id = #{userId}
AND deleted = 0
LIMIT 1;管理员和普通用户如何共用逻辑实际项目中,经常会出现普通用户订单详情和后台订单详情复用同一个 Service 方法的情况。如果直接在 Service 里硬编码 userId 条件,后台功能可能受影响;如果完全不校验,又会引入越权。比较推荐的做法是拆分查询语义,而不是混用一个万能方法:public OrderDetailVO getUserOrderDetail(Long orderId, Long userId) {
Order order = orderMapper.selectByIdAndUserId(orderId, userId);
checkExists(order);
return buildOrderDetail(order);
}

public OrderDetailVO getAdminOrderDetail(Long orderId, Long operatorId) {
checkAdminPermission(operatorId, \"ORDER_DETAIL_VIEW\");

Order order = orderMapper.selectById(orderId);
checkExists(order);

auditLog.record(operatorId, \"VIEW_ORDER_DETAIL\", orderId);
return buildOrderDetail(order);
}这样做的好处是权限意图明确:普通用户路径校验资源归属,后台路径校验角色权限并记录审计日志。代码审计检查清单下面是一份比较实用的检查清单,适合在审计 Spring Boot 项目时快速过一遍。检查项重点关注详情接口是否只按 id 查询,是否绑定当前用户或租户列表接口是否使用请求参数中的 userId、tenantId 作为过滤条件修改接口是否先校验资源归属,再执行 update删除接口是否存在仅按 id 删除或软删除导出接口是否复用了未加权限过滤的查询方法后台接口是否做了角色校验和操作审计批量接口是否逐项校验权限,而不是只校验参数格式批量接口的特殊处理批量接口是越权问题高发点。例如:POST /api/order/batch/cancel

{
\"ids\": [1001, 1002, 1003]
}不安全的写法通常是直接按 ids 更新:UPDATE t_order
SET status = 'CANCELLED'
WHERE id IN
<foreach collection=\"ids\" item=\"id\" open=\"(\" close=\")\" separator=\",\">
#{id}
</foreach>更合理的做法是把当前用户条件也放入更新条件,并检查影响行数是否符合预期:UPDATE t_order
SET status = 'CANCELLED'
WHERE user_id = #{userId}
AND status = 'CREATED'
AND id IN
<foreach collection=\"ids\" item=\"id\" open=\"(\" close=\")\" separator=\",\">
#{id}
</foreach>如果传入 3 个 id,但实际只更新了 1 行,需要返回部分失败或统一失败,避免静默处理导致业务状态不一致。日志与告警越权类问题修复后,建议补充必要日志。日志不是为了记录所有请求参数,而是为了在异常访问时能还原关键上下文。建议记录:当前用户 id、角色、租户 id。被访问资源 id。接口路径和操作类型。鉴权失败原因。请求 traceId,方便关联网关和应用日志。注意不要把 token、完整身份证号、银行卡号等敏感内容直接写入日志。测试用例建议修复后最好补充自动化测试,避免后续重构时回归。@Test
void userShouldNotViewOthersOrder() {
Long userA = 10001L;
Long userB = 10002L;

Long orderId = createOrderForUser(userA);

mockLoginUser(userB);

assertThrows(BizException.class, () -> {
orderService.getUserOrderDetail(orderId, userB);
});
}如果项目有接口层测试,也可以补充 MockMvc 用例,验证 HTTP 状态码和响应结构:mockMvc.perform(get(\"/api/order/detail\")
.param(\"id\", String.valueOf(orderId))
.header(\"Authorization\", userBToken))
.andExpect(status().isForbidden());总结越权问题的本质不是“有没有登录”,而是“当前身份是否有权访问当前资源”。在代码审计时,只要看到用户可控的资源 id,就应该顺手追一下数据查询条件和权限判断位置。比较稳妥的实践是:普通用户接口按资源归属过滤,后台接口按角色权限控制并记录审计,多租户场景必须绑定 tenantId。对于批量、导出、状态修改这类接口,要比详情接口更谨慎,因为它们一旦遗漏权限校验,影响范围通常更大。经验上看,越权问题最适合通过“代码审计 + 测试账号复现 + 自动化用例固化”来处理。单靠前端隐藏按钮或接口文档约束,基本挡不住真实的边界错误。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…