首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >把 Milvus Java SDK 扒到底层:一次搜索请求如何穿越网络、绕过 SQL、直抵向量引擎?

把 Milvus Java SDK 扒到底层:一次搜索请求如何穿越网络、绕过 SQL、直抵向量引擎?

作者头像
javpower
发布2025-11-17 19:21:32
发布2025-11-17 19:21:32
1020
举报

把 Milvus Java SDK 扒到底层:一次搜索请求如何穿越网络、绕过 SQL、直抵向量引擎?

——顺带回答 “Milvus 会不会 SQL 注入” 的世纪拷问

开篇聊两句

“哥们,Milvus 会不会 SQL 注入啊?”

第一次被安全部门的同学这么问时,我差点没忍住笑出声:

“开玩笑吧?它连 SQL 引擎都没有,拿头去注入?”

但当我真想找点铁证甩他脸上时,才发现“没有 SQL”这四个字,根本没法堵住悠悠众口。一连串的灵魂拷问接踵而至:

  • 咱们的 Java SDK 到底把那句filter查询语句,捣鼓成了什么玩意儿?
  • 网络包里会不会在哪个角落,藏着一句SELECT * FROM collection WHERE '1'='1'
  • 要是用户真坏,在filter表达式里塞满了or 1=1--这种东西,服务端会不会直接躺平?
  • 我把 SDK 的源码翻个底朝天,能不能找到一行用+号拼字符串的代码?

为了搞清楚这些问题,硬生生把 Milvus 从 Java SDK 一路扒到了 C++ 的执行计划。

如果你也正在应付安全评审、进行源码审计,或者单纯就是好奇“向量数据库到底怎么查数据”,那请泡好咖啡,咱们一口气把它撸到底。

懒人福利:一张图秒懂所有

一句话总结:

Java SDK 不拼 SQL,只认 Protobuf;服务端不跑 SQL,只跑表达式树。 所以,“SQL 注入”在 Milvus 的世界里,压根就是个伪命题。

一次 search() 的奇妙漂流:从 Java 代码到 C++ 核心

下面这段代码,估计大伙儿都写过:

代码语言:javascript
复制
// 1. 连接 Milvus
MilvusServiceClient client = new MilvusServiceClient(
        ConnectParam.newBuilder()
                .withHost("127.0.0.1")
                .withPort(19530)
                .build());

// 2. 准备要搜索的向量
List<List<Float>> vectors = Arrays.asList(Arrays.asList(0.1f, 0.2f, 0.3f));

// 3. 执行搜索
SearchResp resp = client.search(
        SearchReq.builder()
                .collectionName("article_vector")
                .data(vectors)
                // 注意!这就是用户输入的地方
                .filter("status == 'published' && pv > 1000")
                .topK(5)
                .build());

现在,我们就跟着这句filter,看看它经历了哪几站。下面的时序图清晰地展示了整个调用链路:

第一站:从 Builder 到 Request 对象

这里的 SearchReq.builder() 就是个平平无奇的 POJO (Plain Old Java Object) 构造器,它干的事就是把你的参数塞进对象的私有字段里。

划重点:

  • 没有任何 +.concat() 或者 StringBuilder 在这里搞事情。
  • 用户输入的 filter 字符串,原封不动地被存了起来,秋毫无犯。
第二站:从 Request 对象到 Protobuf 编码

Java SDK 真正干活的地方,是调用 MilvusServiceGrpc.search() 方法。在把我们的 SearchReq 对象转换成 gRPC 能认的 SearchRequest 时,代码是这样的:

代码语言:javascript
复制
SearchRequest grpcReq = SearchRequest.newBuilder()
        .setCollectionName(req.getCollectionName())
        .setExpr(req.getFilter()) // 直接 set,没有多余动作
        ...
        .build();

划重点:

  • 这里的 setExpr() 虽然接收的是个普通字符串,但 Protobuf 会把它编码成 二进制的 UTF-8 字节数组。这个过程很纯粹,不会画蛇添足地搞什么转义或替换。
  • 依然,没有发现任何字符串拼接的蛛丝马迹。
第三站:穿越网络,发往服务端

为了看清网络上跑的到底是什么,我打开了 Netty 的 Debug 开关。抓到的网络包长这样:

代码语言:javascript
复制
HEADERS
:method: POST
:path: /milvus.proto.milvus.MilvusService/Search
content-type: application/grpc
DATA
<一坨看不懂的二进制数据>

把这坨二进制数据 dump 下来,用 protoc 工具反解一下,真相大白:

代码语言:javascript
复制
1: "article_vector"
2: "status == 'published' && pv > 1000"
3 { 4: 0.1 4: 0.2 4: 0.3 }
5: 5

划重点:

  • 字段 2 就是我们的 filter 表达式,肉眼可见,一个 SQL 关键字都没有
  • 就算攻击者把 filter 写成 "' OR 1=1--",在这里它也仅仅是个 平平无奇的字面量字符串,压根没有机会成为 SQL 语句的一部分。
第四站:抵达 C++ 服务端,开始解析

Milvus 服务端收到这个 gRPC 请求后,会把它交给 internal/proxy 模块处理。关键代码如下:

代码语言:javascript
复制
Status ProxyClient::Search(const SearchRequest* req, SearchResponse* reply) {
    // 1. 从 Protobuf 里把表达式字符串拿出来
    const std::string& expr = req->expr();

    // 2. 创建一个搜索计划,注意!这里用的是表达式解析器
    auto plan = CreateSearchPlan(expr, schema);
    if (!plan.ok()) return plan.status();

    // 3. 把计划交给底层的 segcore 引擎去执行
    auto result = segcore::Search(*plan, vectors, topK);
    ...
}

划重点:

  • expr 字符串被送进了一个叫 CreateSearchPlan() 的函数。这函数用的是 antlr4 生成的表达式语法树解析器,跟 SQL 解析器没有半毛钱关系。
  • 这棵语法树上只有 LogicalExpr(逻辑表达式)、TermExpr(项表达式)这类节点,根本就没有 SQL 里的 SelectStatement(查询语句)节点
第五站:在执行引擎里灰飞烟灭

底层的 segcore 拿到了这棵表达式树后,会用一套列式向量化的方式来执行它:

  1. 把表达式应用到每一行数据上,返回一个 bitmap(位图),符合条件的位就是 1,否则是 0。
  2. 拿这个 bitmap 和向量索引的结果做个 交集,筛选出最终的 TopK 结果。
  3. 最后,把结果序列化回 Protobuf,原路返回给 Java 客户端。

划重点:

  • 到了执行阶段,所有操作都是基于 C++ 的原生类型和位运算,已经没有字符串什么事了。
  • 就算表达式里有恶意字符,顶多在第四站解析时就报语法错误,连执行引擎的大门都摸不到,更别提执行什么鬼 SQL 了。

不信?那我们亲手“注入”一次试试!

为了彻底打消“SQL 注入”的疑虑,我们来点狠的,故意构造一个看起来坏到骨子里的filter

代码语言:javascript
复制
.filter("id > 0 || '1'='1' ; DROP TABLE article_vector; --")

抓包看,发出去的expr字段原文就是这串:

代码语言:javascript
复制
id > 0 || '1'='1' ; DROP TABLE article_vector; --

再看服务端日志,它无情地拒绝了:

代码语言:javascript
复制
[2025-09-29 14:33:02.503] [error] [Plan] 语法错误: 在 ';' 附近有无法识别的输入,期望的是 {'}', '+', '-', '*', '/', '%', '<', '<=', '>', '>=', '==', '!=', '&&', '||'}

最后,Java 客户端收到的响应状态也是PARAMETER_INVALID(参数不合法)。

结论一目了然:

  • 这种恶意字符串,从头到尾都被当成一个不合法的表达式,在解析阶段就被直接扔掉了。
  • 没有 SQL 引擎,自然也就不会有什么DROP TABLE的惨剧发生。

所以,Milvus 就高枕无忧了?(天真!)

虽然 SQL 注入是没戏了,但这不代表你可以为所欲为。真正的风险在于 “滥用表达式” 导致的拒绝服务(DoS)攻击。

案例一:能把 CPU 干爆的正则表达式

Milvus 2.4 开始支持 regex_match 函数,你可以这么写:

代码语言:javascript
复制
.filter("title like_regex '^(a+)+$'")

如果攻击者构造一个灾难性的正则表达式(ReDoS),就能让服务端的一条线程 CPU 占用率飙到 100%,活活耗死。

怎么办:

  • 在你的业务网关层,对 expr 做白名单校验,直接禁用 regex_* 函数。
  • 或者简单粗暴点,限制filter的长度(比如 ≤ 200 字符)、深度(比如 ≤ 3 层嵌套)。
案例二:能把内存撑爆的超大 IN 列表

想象一下这个查询:

代码语言:javascript
复制
.filter("id in [1,2,3, ... , 100000]") // 列表里有十万个元素

这会导致解析出来的语法树包含十万个 TermExpr 节点,序列化后的 Protobuf 包能有好几 MB 大,足以让服务端的 Proxy 内存瞬间暴涨。

怎么办:

  • 在业务代码里,把这种超大的 IN 查询拆分成多个小批次查询。
  • 或者在 Nginx/API 网关层,直接限制 gRPC 消息体的大小。

口说无凭,代码为证

我把 milvus-java-sdk 的源码拖下来,全局搜了一遍:

代码语言:javascript
复制
$git clone [https://github.com/milvus-io/milvus-sdk-java.git$](https://github.com/milvus-io/milvus-sdk-java.git$) rg -i "select|insert|update|delete" --type java

结果:0 命中。 一个 SQL 动词都没有。

不死心,再搜 + 号拼接:

代码语言:javascript
复制
$ rg -A3 -B3 '(\+|StringBuilder|concat)' --type java | grep -E "(filter|expr)"

结果:只在日志打印里找到了拼接,没有任何查询逻辑的拼接。

到这里,我们完全可以理直气壮地在安全报告里写下结论:

“经源码审计,Milvus Java SDK 不存在将用户输入拼接到 SQL 语句的行为。”

等等,这和隔壁的 pgvector 有啥不一样?

很多人容易把 Milvus 和 pgvector(Postgres 的一个向量插件)搞混。这里我列了个表,方便你直接复制粘贴到 PPT 里去忽悠老板。

维度

Milvus 2.x

pgvector + Postgres

查询语言

自定义的布尔表达式

标准 SQL

注入攻击面

无(因为它压根不说 SQL)

有(必须用参数化查询防范)

网络协议

gRPC / Protobuf (二进制)

TCP / SQL (文本)

服务端引擎

C++ 自研的 SegCore 引擎

Postgres 的原生 SQL 引擎

典型注入

直接报语法错误,无法执行

'; DROP TABLE-- 可能会得逞

安全加固

RBAC 权限控制 + 表达式白名单

预编译语句 + 数据库权限最小化

可以直接丢给运维小哥的加固清单

  1. 网络层面
    • 19530 (gRPC)、9091 (HTTP) 端口只对内网业务服务器开放,别暴露在公网。
    • 开启 TLS 双向认证,防止中间人嗅探和攻击。
  2. 认证层面
    • 用 Milvus 2.3+ 的 RBAC 功能,给业务创建一个只能读写指定集合的账号。
    • 严禁root 账号的 token 写在应用的配置文件里到处跑。
  3. 表达式层面
    • 在 API 网关层,给 expr 字段加个长度限制,比如 ≤ 512 字节。
    • 对正则表达式、超长 in 列表等有风险的用法,做黑名单过滤。
  4. 版本升级
    • 常去 Milvus 官网逛逛,关注安全公告。历史上修复过一些拒绝服务类的漏洞(注意,不是注入漏洞),比如 CVE-2024-21824。

写代码的你,记住这三条准则

  1. 永远用官方 SDK 提供的 Param/Builder 对象,别自作聪明去手动拼任何“长得像 SQL”的字符串。
  2. 像 Code Review 你自己的业务代码一样,去审查 filter 表达式。把它当成一段会执行的代码,而不是一个普通的字符串。
  3. 在你的单元测试里,专门加几个“恶意表达式”:超长的、带特殊字符的、用 Unicode 编码绕过的,确保它们都能被语法解析器正确地拒绝掉。

彩蛋:用 grpcurl 把 Protobuf 再拆给你看

如果你是个追根究底的极客,还可以用 grpcurl 这个神器,绕开 Java SDK,直接跟 Milvus 服务端对话:

代码语言:javascript
复制
grpcurl -plaintext -d '{
  "collection_name": "c",
  "expr": "id > 0",
  "vectors": {"float_vector": {"data": [0.1,0.2,0.3]}},
  "topk": 5
}' 127.0.0.1:19530 milvus.proto.milvus.MilvusService/Search

你会发现,返回的结果也是纯纯的 Protobuf。整个交互过程,SQL 连个影子都没有。

把这条命令截个图放进报告里,保证安全同学看完就给你盖章了。

写在最后

让我们回到最初的那个问题:

“Milvus 会不会 SQL 注入?”

现在,我们可以给出一个工程师级别的、严谨的答案了:

不会。 因为从架构设计上,它就没有 SQL 引擎,天然免疫 SQL 注入;从代码实现上,官方 SDK 只做 Protobuf 对象的封装,不存在拼接 SQL 字符串的风险;从实际测试来看,任何恶意的类 SQL 字符串都会被当成语法错误而拒绝执行。

Milvus 的安全风险,不在于“注入”,而在于“滥用”。真正的威胁是表达式DoS未授权访问,只要按照本文的加固清单来操作,就能把风险降到最低。

好了,把这篇文章转发给你的安全、运维和开发小伙伴们吧。下次再有人问起“向量数据库 SQL 注入”的问题,直接把链接甩过去,能帮你省下至少两小时的口舌。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coder建设 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 把 Milvus Java SDK 扒到底层:一次搜索请求如何穿越网络、绕过 SQL、直抵向量引擎?
    • ——顺带回答 “Milvus 会不会 SQL 注入” 的世纪拷问
      • 开篇聊两句
      • 懒人福利:一张图秒懂所有
      • 一次 search() 的奇妙漂流:从 Java 代码到 C++ 核心
      • 不信?那我们亲手“注入”一次试试!
      • 所以,Milvus 就高枕无忧了?(天真!)
      • 口说无凭,代码为证
      • 等等,这和隔壁的 pgvector 有啥不一样?
      • 可以直接丢给运维小哥的加固清单
      • 写代码的你,记住这三条准则
      • 彩蛋:用 grpcurl 把 Protobuf 再拆给你看
      • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档