当前位置: 首页 > news >正文

使用CodeQL挖掘Spring中的大量赋值漏洞

什么是大量赋值漏洞

或许你没有听过“大量赋值漏洞”,但你一定在网络上见过这样的技巧:FUZZ请求参数的额外字段。比如:

  1. 在某个获取用户列表的请求中,如 /api/user?name=ad&orderby=id,可以尝试 FUZZ SQL关键字,例如:/api/user?name=ad&orderby=id&sort=desc,1/0&limit=0,1 PROCEDURE ANALYSE(..),可能发现潜在注入点;
  2. 在注册账号场景中,可以尝试 FUZZ 内部字段,如设置用户身份的字段 role=adminisAdmin=true等,测试是否能意外获得管理员权限。

以上漏洞都存在一个共同点,即后端接收了这些本不应暴露给用户的字段,并进行了处理。而本文讨论的大量赋值漏洞,则是在此基础上的一个特殊案例。

大量赋值(Mass Assignment)是指,后端为了简化开发流程,自动将请求参数绑定到后端对象的属性上。但是,如果开发者没有限制哪些字段可以被赋值,攻击者就可能构造请求参数,注入原本不应由用户控制的敏感字段。如果后端接受并处理了这些敏感字段,则会引发一系列安全问题,也就是所谓的大量赋值漏洞(Mass Assignment Vulnerability)。

Spring中的大量赋值

Spring Framework 是一个开源的 Java 应用程序开发框架,最初由 Rod Johnson 于 2003 年发布。它以其轻量级、模块化和强大的依赖注入(DI)与面向切面编程(AOP)特性而广受欢迎,尤其在 Java Web 开发中。

在 Web 开发中,Spring 提供了强大的请求映射与参数处理机制,其中使用最多的为@RequestBody 注解。该注解能够将 HTTP 请求体中的 JSON、XML 等结构化数据反序列化为 Java 对象,并绑定到控制器方法的参数中。这种自动映射的机制极大地简化了数据解析与对象构造的过程。然而,正是这种便捷性,也可能在不经意间引入安全隐患:如果未对绑定对象的字段进行严格控制,攻击者便可以利用 @RequestBody 自动赋值的特性,向对象注入敏感字段,进而引发大量赋值漏洞

接下来通过一段示例代码,逐步分析这一漏洞的形成过程及其利用方式:

在某一应用程序中,存在一个用户注册功能。该程序中通过User类, 定义了一个简单的 “用户” 数据模型,其代码如下:

// User.java
public class User{private String name;private String password;private String role;public void setName(String name){this.name = name;}... Getters and Setters
}

现有以下路由代码,用于处理前端发起的用户注册请求,该请求直接使用 @RequestBody注解, 将用户的POST传参解析为 User 类型参数,随后直接通过 service层 保存在数据库中:

@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/register")public ResponseEntity<User> register(@RequestBody User newUser) {userService.save(newUser);return ResponseEntity.ok(newUser);}
}

User表对应的建表语句如下,可以看到其中的role字段默认为user :

CREATE TABLE `litemall_user` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(63) NOT NULL COMMENT '用户名称',`password` varchar(63) NOT NULL DEFAULT '' COMMENT '用户密码',`role` varchar(63) NOT NULL DEFAULT 'user' COMMENT '用户角色',) 

现在让我们来看看前端是如何发起注册请求的,代码如下. 在前端代码中,**没有显示声明 role字段内容,**因此正常注册的用户应该是普通权限.

// api.js
export function register(username, password) {const endpoint = `${baseurl}/user/register`;const payload = {name: username,password: password};return axios.post(endpoint, payload, {headers: {'Content-Type': 'application/json'}});
}

然而由于 @RequestBody 会将请求体中字段自动绑定到 User 对象的每个属性上,而后端未对 role 字段做任何限制。因此,若攻击者成功FUZZ出该字段或者通过源码审计发现出该处存在绑定问题,这将导致攻击者成功注册为管理员用户。

如何修复?

很简单,如果用户权限只能由管理员设置,我们只需要在调用 userService.save()前,手动设置 role"user":

@PostMapping("/register")
public ResponseEntity<User> register(@RequestBody User newUser) {newUser.setRole("user");userService.save(newUser);return ResponseEntity.ok(newUser);
}

我的CodeQL探索之旅:如何发现CVE-2025-6702

上一章节演示了Spring中的大量赋值漏洞案例:后端通过 @RequestBody自动绑定了客户端传入的数据对象,但未对敏感字段(如 role)进行显式设置或校验,从而导致攻击者可以设置敏感参数实现权限提升。

本章节中,笔者将使用真实案例,揭示如何使用CodeQL发现CVE-2025-6702。首选需要明确任务,即:通过CodeQL,找到使用 @RequestBody 解析传入参数,且对应路由函数里未对相关字段使用 setter方法进行显示赋值。

进一步的,可将目标拆解为以下三个步骤:

  1. 查找使用 @RequestBody 注解的参数
  2. 在 步骤1 的基础上,需要满足该参数类型为指定的数据结构
  3. 在 步骤2 的基础上,检测该参数 未使用显式调用 setter 方法进行字段赋值

查找使用@RequestBody注解的参数

参照官方annotations-in-java案例,很容易查找到使用 @RequestBody作为注解的参数:

/*** This is an automatically generated file* @name Hello world* @kind problem* @problem.severity warning* @id java/example/hello-world*/import javaclass RequestbodyAnnotation extends Annotation {RequestbodyAnnotation() {this.getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestBody")}
}from Parameter bodyParameter
where bodyParameter.getAnAnnotation() instanceof RequestbodyAnnotation
select bodyParameter, "has a @RequestBody annotation"

参数类型为指定的数据结构

通过对上一步查询结果的观察,发现存在非绑定类型的参数,如:String 类型。

因此我们需要进一步筛选出符合自动绑定类型的参数。

通过阅读源码,发现该项目相关的绑定参数定义在 org.linlinjava.litemall.db.domain包下,且都符合LitemallXXX的命名格式,因此定义如下一个谓词进行过滤:

predicate daoClass(Class c) {c.getPackage().getName() = "org.linlinjava.litemall.db.domain" andc.getName().matches("Litemall%")
}

对应的where 语句更新为:

from Parameter bodyParameter
where bodyParameter.getAnAnnotation() instanceof RequestbodyAnnotation and
daoClass(bodyParameter.getType())
select bodyParameter, "has a @RequestBody annotation"

为了方便,将上面的代码进行整合,重新定义一个 BodyParameter 类,用来获取所有满足前两个步骤的请求参数:

class BodyParameter extends Parameter {BodyParameter() {this.getAnAnnotation() instanceof RequestbodyAnnotation anddaoClass(this.getType())}
}

未显式调用的setter方法

WxCartController.java:113:50为例,可以看到在add 路由下, 该函数通过一系列的setter 方法显示设置字段值,最后调用 cartService.add(cart) 进行Service层的业务逻辑处理。因此如果能够找到未被显式调用的 setter方法,就可能发现潜在的大量赋值漏洞。

为了需要找到大量赋值类对应的 setter方法,作者定义了SetterMethod类,如下:

class SetterMethod extends Method {SetterMethod() {exists(Class c |daoClass(c) andthis.getDeclaringType() = c andthis.isPublic() andthis.getName().regexpMatch("set.*") andthis.getNumberOfParameters() = 1)}
}

该类主要依据 setter 方法特征的如下三个特征进行过滤:

  1. public 方法
  2. 方法名称格式为setXxx
  3. setter方法只拥有一个参数

现在来完善 from-where-select 语句,用来查询所有未被显式调用的setter方法:

from SetterMethod allSetterMethods, BodyParameter requestBodyParam
whererequestBodyParam.getType() = allSetterMethods.getDeclaringType() andnot exists(MethodCall explicitCall |explicitCall.getCallee() = allSetterMethods andexplicitCall.getQualifier().(VarAccess).getVariable() = requestBodyParam)
select requestBodyParam, "Dont't call setter: " + allSetterMethods.getName() + "explicitly" 

执行查询语句,会发现得到了大量结果。由于只考虑用户侧可控的参数和请求,因此可以限定普通用户可访问的api来进行进一步过滤,如下,结果从539条减少到了94条.

变量传递优化

截止目前,已经完成了之前的所有任务,现在来浏览一下是否存在有趣的setter方法未被显式调用吧。通过浏览查询结果, setPrice引起了我的关注。嗯? 难道我们找到了一个未显式设置商品价格的参数?让我们来进一步确认:

点击右侧蓝色字体,编辑器跳转到对应参数的代码片段.通过阅读代码后发现,这段代码确实没有显式调用 setPirce设置商品价格,但是这段代码压根儿就没有处理我们传入的cart参数.

因此,我们需要在此处进一步优化查询逻辑,使得通过@RequestBody传入的参数,传入到了service层进行处理。这并不复杂,通过本地数据流就能满足我们的需求.不过在构造where语句前,先定义下 serviceClass,用于查找 service层的数据结构:

predicate serviceClass(Class c) {c.getPackage().getName() = "org.linlinjava.litemall.db.service" andc.getName().matches("%Service")
}

接着完善from-where-select语句:

import semmle.code.java.dataflow.DataFlowfrom SetterMethod allSetterMethods, BodyParameter requestBodyParam
whererequestBodyParam.getType() = allSetterMethods.getDeclaringType() andnot exists(MethodCall explicitCall |explicitCall.getCallee() = allSetterMethods andexplicitCall.getQualifier().(VarAccess).getVariable() = requestBodyParam) andexists(MethodCall call |serviceClass(call.getQualifier().(VarAccess).getVariable().getType()) andDataFlow::localFlow(DataFlow::parameterNode(requestBodyParam),DataFlow::exprNode(call.getAnArgument()))) andnot requestBodyParam.getFile().getAbsolutePath().matches("%admin-api%")
select requestBodyParam, "Dont't call setter: " + allSetterMethods.getName() + " explicitly"

注意:由于优化的查询语句中使用到了数据流分析,因此需要导入相关模块: semmle.code.java.dataflow.DataFlow

执行查询,此时的查询结果从 94条 减少到了 66条。

getter也算显式调用

继续浏览查询结果,又发现一个问题:虽然有些参数没用使用setter函数进行赋值处理,但是使用了getter函数获取对应的值.显然此时应该认为该参数为系统期望输入。因此我们需要过滤掉使用getter函数对应的setter函数

如何才能满足上诉需求?笔者采用了一种最为直观的方法,将getter方法变为setter方法。在优化where条件之前,先定义所有的setter方法

class GetterMethod extends Method {GetterMethod() {exists(Class c |daoClass(c) andthis.getDeclaringType() = c andthis.isPublic() andthis.getName().regexpMatch("get.*"))}
}

接着使用replaceAll("get", "set")将对应的getter方法转为setter方法,如下:

from SetterMethod allSetterMethods, BodyParameter requestBodyParam
whererequestBodyParam.getType() = allSetterMethods.getDeclaringType() andnot exists(MethodCall explicitCall |explicitCall.getCallee() = allSetterMethods andexplicitCall.getQualifier().(VarAccess).getVariable() = requestBodyParam) andexists(MethodCall call |serviceClass(call.getQualifier().(VarAccess).getVariable().getType()) andDataFlow::localFlow(DataFlow::parameterNode(requestBodyParam),DataFlow::exprNode(call.getAnArgument()))) andnot exists(GetterMethod getterCall, MethodCall explicitCall |daoClass(getterCall.getDeclaringType()) andexplicitCall.getCallee() = getterCall andexplicitCall.getQualifier().(VarAccess).getVariable() = requestBodyParam andgetterCall.getName().replaceAll("get", "set") = allSetterMethods.getName()) andnot requestBodyParam.getFile().getAbsolutePath().matches("%admin-api%")
select requestBodyParam, "Dont't call setter: " + allSetterMethods.getName() + " explicitly"

此时,结果从 66 条减少到了 56 条。

发现漏洞!

来感受下逐步优化查询语句后的成果吧。浏览查询结果过程中, setAdminContent 未被显式调用引起了笔者的关注。根据建表语句,发现该字段为“管理员回复内容”。最后经过测试,发现该接口存在大量赋值漏洞。如果你想查看漏洞细节,可以前往这里:Notion

其次,还发现查询结果中多处存在未显示调用 setDeleted 方法,这允许用户自删除自己的评论/数据,应该没有用户无聊到通过大量赋值来“删除”自己的数据吧。

最后完整的CodeQL查询语句:

/*** This is an automatically generated file** @name Hello world* @kind problem* @problem.severity warning* @id java/example/hello-world*/import javaclass RequestbodyAnnotation extends Annotation {RequestbodyAnnotation() {this.getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestBody")}
}predicate daoClass(Class c) {c.getPackage().getName() = "org.linlinjava.litemall.db.domain" andc.getName().matches("Litemall%")
}class BodyParameter extends Parameter {BodyParameter() {this.getAnAnnotation() instanceof RequestbodyAnnotation anddaoClass(this.getType())}
}class SetterMethod extends Method {SetterMethod() {exists(Class c |daoClass(c) andthis.getDeclaringType() = c andthis.isPublic() andthis.getName().regexpMatch("set.*") andthis.getNumberOfParameters() = 1)}
}import semmle.code.java.dataflow.DataFlow
predicate serviceClass(Class c) {c.getPackage().getName() = "org.linlinjava.litemall.db.service" andc.getName().matches("%Service")
}class GetterMethod extends Method {GetterMethod() {exists(Class c |daoClass(c) andthis.getDeclaringType() = c andthis.isPublic() andthis.getName().regexpMatch("get.*"))}
}from SetterMethod allSetterMethods, BodyParameter requestBodyParam
whererequestBodyParam.getType() = allSetterMethods.getDeclaringType() andnot exists(MethodCall explicitCall |explicitCall.getCallee() = allSetterMethods andexplicitCall.getQualifier().(VarAccess).getVariable() = requestBodyParam) andexists(MethodCall call |serviceClass(call.getQualifier().(VarAccess).getVariable().getType()) andDataFlow::localFlow(DataFlow::parameterNode(requestBodyParam),DataFlow::exprNode(call.getAnArgument()))) andnot exists(GetterMethod getterCall, MethodCall explicitCall |daoClass(getterCall.getDeclaringType()) andexplicitCall.getCallee() = getterCall andexplicitCall.getQualifier().(VarAccess).getVariable() = requestBodyParam andgetterCall.getName().replaceAll("get", "set") = allSetterMethods.getName()) andnot requestBodyParam.getFile().getAbsolutePath().matches("%admin-api%")
select requestBodyParam, "Dont't call setter: " + allSetterMethods.getName() + " explicitly"

Reference

API3:2023 Broken Object Property Level Authorization - OWASP API Security Top 10
API testing | Web Security Academy

CodeQL for Java and Kotlin — CodeQL

WSTG - Latest | OWASP Foundation

http://www.dtcms.com/a/282572.html

相关文章:

  • 行为模式-策略模式
  • 华大北斗TAU1201-1216A00高精度双频GNSS定位模块 自动驾驶专用
  • AWS IAM 最佳安全实践通用指南
  • 从 “洗澡难” 到 “洗得爽”:便携智能洗浴机如何重塑生活?
  • 微服务架构升级:从Dubbo到SpringCloud的技术演进
  • 浏览器自动化方案
  • 创客匠人解析:系统化工具如何重构知识变现效率
  • 在 kubernetes 上安装 jenkins
  • 闲庭信步使用图像验证平台加速FPGA的开发:第十九课——图像拉普拉斯金字塔的FPGA实现
  • Image 和 IMU 时间戳同步
  • 事务~~~
  • JavaScript进阶篇——第五章 对象成员管理与数组遍历优化
  • 密码喷洒复现
  • Thymeleaf 基础语法与标准表达式详解
  • 如何区分Bug是前端问题还是后端问题?
  • LeetCode经典题解:141、判断链表是否有环
  • 【LeetCode】链表相关算法题
  • Node.js Process Events 深入全面讲解
  • 1.3 vue响应式对象
  • FATFS文件系统原理及其移植详解
  • PyTorch 损失函数详解:从理论到实践
  • 嵌入式学习-PyTorch(5)-day22
  • 【深度学习基础】PyTorch中model.eval()与with torch.no_grad()以及detach的区别与联系?
  • Vue 结合 Zabbix API 获取服务器 CPU、内存、GPU 等数据
  • 数据结构自学Day8: 堆的排序以及TopK问题
  • 前端Vue中,如何实现父组件打开子组件窗口等待子组件窗口关闭后继续执行父组件逻辑
  • DeepSeek(18):SpringAI+DeepSeek大模型应用开发之会话日志
  • 单片机(STM32-中断)
  • JS逆向 - YandexSmartCaptcha (worker线程)
  • 基于WebRTC构建应用的可复用模块