Spring框架中的大量赋值漏洞分析与CodeQL挖掘技术
1. 大量赋值漏洞概述
1.1 基本概念
大量赋值(Mass Assignment)漏洞是指后端为了简化开发流程,自动将请求参数绑定到后端对象的属性上,但开发者没有限制哪些字段可以被赋值,导致攻击者可能注入原本不应由用户控制的敏感字段。
1.2 常见表现形式
-
在获取用户列表请求中FUZZ SQL关键字
- 示例:
/api/user?name=ad&orderby=id&sort=desc,1/0&limit=0,1 PROCEDURE ANALYSE(..)
- 示例:
-
在注册账号场景中FUZZ内部字段
- 示例:尝试设置
role=admin或isAdmin=true
- 示例:尝试设置
1.3 漏洞本质
后端接收了本不应暴露给用户的字段并进行处理,特别是当框架自动将请求参数绑定到对象属性时,如果没有适当的限制机制。
2. Spring框架中的大量赋值漏洞
2.1 Spring框架简介
Spring Framework是一个开源的Java应用程序开发框架,以其轻量级、模块化和强大的依赖注入(DI)与面向切面编程(AOP)特性而广受欢迎。
2.2 @RequestBody注解的风险
在Spring Web开发中,@RequestBody注解能够将HTTP请求体中的JSON、XML等结构化数据反序列化为Java对象。这种自动映射机制可能引入安全隐患:
- 自动绑定请求体数据到对象属性
- 如果没有字段限制,攻击者可注入敏感字段
- 可能导致权限提升等安全问题
2.3 漏洞示例分析
2.3.1 用户注册功能示例
User类定义:
public class User {
private String username;
private String password;
private String role; // 默认为"user"
// getters and setters
}
Controller代码:
@PostMapping("/register")
public String register(@RequestBody User user) {
userService.save(user); // 直接保存用户对象
return "注册成功";
}
数据库表结构:
CREATE TABLE User (
username VARCHAR(255),
password VARCHAR(255),
role VARCHAR(255) DEFAULT 'user'
);
前端请求:
{
"username": "attacker",
"password": "123456"
}
攻击者请求:
{
"username": "attacker",
"password": "123456",
"role": "admin" // 注入管理员权限
}
2.3.2 修复方案
在保存前显式设置role字段:
@PostMapping("/register")
public String register(@RequestBody User user) {
user.setRole("user"); // 显式设置角色
userService.save(user);
return "注册成功";
}
3. 使用CodeQL挖掘大量赋值漏洞
3.1 CodeQL分析目标
通过CodeQL查找:
- 使用
@RequestBody注解的参数 - 参数类型为指定的数据结构
- 未使用显式调用setter方法进行字段赋值
3.2 分析步骤详解
3.2.1 查找使用@RequestBody注解的参数
参考官方annotations-in-java案例:
from Parameter p
where p.getAnAnnotation().getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestBody")
select p
3.2.2 筛选特定类型的参数
定义绑定参数类型(示例中为org.linlinjava.litemall.db.domain包下,符合LitemallXXX命名格式):
predicate isTargetType(RefType r) {
r.getPackage().getName() = "org.linlinjava.litemall.db.domain" and
r.getName().matches("Litemall%")
}
整合为BodyParameter类:
class BodyParameter extends Parameter {
BodyParameter() {
this.getAnAnnotation().getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestBody") and
isTargetType(this.getType())
}
}
3.2.3 查找未显式调用的setter方法
定义SetterMethod类:
class SetterMethod extends Method {
SetterMethod() {
this.isPublic() and
this.getName().matches("set%") and
this.getNumberOfParameters() = 1
}
}
查询未显式调用的setter:
from BodyParameter bp, SetterMethod sm
where not exists(MethodAccess ma |
ma.getMethod() = sm and
ma.getEnclosingCallable() = bp.getCallable()
)
select bp, sm
3.2.4 优化查询:限制用户可访问API
添加可访问性限制:
// 添加可访问性条件
where ...
and bp.getCallable().getDeclaringType().getASupertype*().hasQualifiedName("org.springframework.web.bind.annotation", "RestController")
3.2.5 变量传递优化
确保参数传递到service层处理:
class ServiceClass extends RefType {
ServiceClass() {
this.getName().matches("%Service") and
this.getPackage().getName() = "org.linlinjava.litemall.db.service"
}
}
// 添加数据流分析
where ...
and exists(DataFlow::Node source, DataFlow::Node sink |
DataFlow::localFlow(source, sink) and
source.asExpr() = bp.getAnAccess() and
sink.asExpr().(MethodAccess).getMethod().getDeclaringType() instanceof ServiceClass
)
3.2.6 处理getter方法调用
过滤掉使用getter方法对应的setter方法:
class GetterMethod extends Method {
GetterMethod() {
this.isPublic() and
this.getName().matches("get%") and
this.getNumberOfParameters() = 0
}
}
// 在where条件中添加
where ...
and not exists(MethodAccess ma |
ma.getMethod().getName() = sm.getName().replaceAll("set", "get") and
ma.getEnclosingCallable() = bp.getCallable()
)
3.3 完整CodeQL查询示例
import java
import semmle.code.java.dataflow.DataFlow
predicate isTargetType(RefType r) {
r.getPackage().getName() = "org.linlinjava.litemall.db.domain" and
r.getName().matches("Litemall%")
}
class BodyParameter extends Parameter {
BodyParameter() {
this.getAnAnnotation().getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestBody") and
isTargetType(this.getType())
}
}
class SetterMethod extends Method {
SetterMethod() {
this.isPublic() and
this.getName().matches("set%") and
this.getNumberOfParameters() = 1
}
}
class GetterMethod extends Method {
GetterMethod() {
this.isPublic() and
this.getName().matches("get%") and
this.getNumberOfParameters() = 0
}
}
class ServiceClass extends RefType {
ServiceClass() {
this.getName().matches("%Service") and
this.getPackage().getName() = "org.linlinjava.litemall.db.service"
}
}
from BodyParameter bp, SetterMethod sm
where
not exists(MethodAccess ma |
ma.getMethod() = sm and
ma.getEnclosingCallable() = bp.getCallable()
) and
bp.getCallable().getDeclaringType().getASupertype*().hasQualifiedName("org.springframework.web.bind.annotation", "RestController") and
exists(DataFlow::Node source, DataFlow::Node sink |
DataFlow::localFlow(source, sink) and
source.asExpr() = bp.getAnAccess() and
sink.asExpr().(MethodAccess).getMethod().getDeclaringType() instanceof ServiceClass
) and
not exists(MethodAccess ma |
ma.getMethod().getName() = sm.getName().replaceAll("set", "get") and
ma.getEnclosingCallable() = bp.getCallable()
)
select bp, sm, "Potential mass assignment vulnerability: " + sm.getName()
4. 实际漏洞案例:CVE-2025-6702
4.1 漏洞发现过程
通过上述CodeQL查询,发现以下关键点:
-
setAdminContent方法未被显式调用- 该字段为"管理员回复内容"
- 攻击者可自行设置管理员回复
-
setDeleted方法未被显式调用- 允许用户自删除自己的评论/数据
4.2 漏洞利用细节
攻击者可构造如下请求:
POST /wx/comment/post
{
"content": "正常评论",
"adminContent": "伪造的管理员回复",
"deleted": false
}
通过修改adminContent和deleted字段,实现非预期的功能操作。
5. 防御措施
5.1 最佳实践
- 显式设置敏感字段:在保存前显式设置不应由用户控制的字段
- 使用DTO模式:创建专用的数据传输对象,仅包含允许用户设置的字段
- 字段白名单:使用
@JsonIgnoreProperties(ignoreUnknown = true)或类似注解 - 输入验证:对所有用户输入进行严格验证
5.2 Spring特定防御
- 使用
@JsonView控制序列化/反序列化的字段 - 实现
WebDataBinder自定义数据绑定规则 - 使用
@InitBinder限制可绑定的字段
6. 总结
大量赋值漏洞是现代Web框架中常见的安全问题,特别是在使用自动绑定功能的框架如Spring中。通过CodeQL等静态分析工具,可以有效地发现这类漏洞。开发者应当始终遵循最小权限原则,显式控制哪些字段可以由用户设置,从而避免此类安全问题。
7. 参考资源
- OWASP API Security: Broken Object Property Level Authorization
- PortSwigger Web Security: Mass Assignment Vulnerabilities
- CodeQL for Java documentation
- OWASP Web Security Testing Guide: Testing for Mass Assignment