文章

关于 MinIO、S3 兼容对象存储与 SILO 社区分支的文章与分析。

关于 MinIO、S3 兼容对象存储与 SILO 社区分支的文章与分析。

续命 MinIO:承诺兑现

两个月前,我在《MinIO 已死,MinIO 复生》里立了一个 flag,承接上游的烂摊子,跟 CVE、修 Bug。 那篇文章上了几个小时的 Hacker News 头条。鼓励不少,质疑也不少:一个人,真维护得了这种项目吗?

这个问题其实问得很好。因为真正见真章的时刻,不是点 fork 按钮,也不是改 README 文档,而是当安全漏洞真砸下来的时候。

现在,这件事可以交账了。

4 月 15 日到 17 日,三天时间,pgsty/minio 发布了 RELEASE.2026-04-17,连续修掉并关闭了 4 条 CVE 加几条同期披露的安全漏洞,OIDC JWT 算法混淆(CVSS 9.8)、LDAP 登录用户名枚举与暴力破解、复制头元数据注入导致对象不可读、S3 Select 超大记录打穿内存,以及两条 unsigned-trailer 写入路径上的签名绕过。

A promise made, a promise kept.

当时我把话说得很清楚:不做新特性,只保障供应链;遇到可复现问题和安全漏洞,会积极跟进和修补。这件事,现在算是交账了。


上游发生了什么

2025 年 12 月,MinIO 把开源仓库改成 维护模式。README 里写着 “安全修复会逐例评估”。 到 2026 年 2 月,仓库直接归档,首页变成了 “当前仓库已经不再维护”。但同一个仓库的 SECURITY.md 还留着:“我们总会为最新版本提供安全更新”。

而最近一个月,MinIO 又暴漏出四个高危漏洞,两个中危,覆盖最后的开源版本。

policy.webp

与此同时,上游官方仓库距离最后一次发布已经过去 184 天。 他们披露漏洞,但只在商业版本中修复。对于开源版用户,他们给的建议就一条,“升级到商业版 AIStor”。

顺便一提,MinIO 入门起步价约 10 万美元/年,400 TiB,单价基本跟 AWS S3 差不多,简直离大谱,毕竟这是纯软件。

一种很精致的玩法。仓库归档了,道义责任撇清了;但 CVE 通告照发,既能刷一波 “我们很负责任” 的存在感,又恰好能把用户赶进商业版的羊圈。

挺聪明的。只是还得有人得把这坑填上。


这次修了什么

这篇文章我不想写成漏洞分析报告。具体每一条的 CVSS 分数、攻击链、PoC 代码,我在 发布注记 里都一一列了,感兴趣的朋友可以去看。这里只说一句话版本:

  • CVE-2026-33322(OIDC JWT 算法混淆,CVSS 9.8):在特定 IdP 配置下可以伪造任意身份,包括 consoleAdmin。攻击者只要知道 OIDC ClientSecret,数学上就能签出一张 “我是管理员” 的通行证,MinIO 会乖乖验证通过。影响范围从 2022 年 11 月到今年 3 月,三年半
  • CVE-2026-33419(LDAP STS 登录枚举与暴力破解):攻击者可以先用登录接口枚举出真实用户名,再无速率限制地爆破密码,最后直接拿到 STS 凭证。整个链条从头到尾没有一道闸。
  • CVE-2026-34204(复制头元数据注入):普通 PUT / COPY 请求里夹一些 X-Minio-Replication-* 头,就能把对象写成永久不可读状态,数据还在,但你再也读不出来。
  • CVE-2026-39414(S3 Select 内存耗尽):一条恶意请求,就可以把 MinIO 进程吃到 OOM。
  • GHSA-hv4r-mvr4-25vw / GHSA-9c4q-hq6p-c237:unsigned-trailer 路径上的两条签名校验绕过,匿名或伪造签名的请求可以在某些路径下成功写入对象。

再加上 go-josego.opentelemetry.io 和 Go 1.26.2 自身吸收的一连串标准库与依赖 CVE,这一版本聚合了接近二十条安全条目。

有的能伪造身份拿到高权限访问,有的能把登录入口拿去枚举和爆破,有的能把对象写成永久不可读,有的能用一条请求把服务吃到 OOM,还有的能在缺失签名校验时直接写入对象。这不是小修小补,这是实打实的维护责任。

issue.webp


这次是怎么修的

在之前那篇文章里,我明确说过我会用 AI Coding Agent 来维护这个项目,事实上我也是这么做的。这一轮修复里,我扮演了一个 Blind Manager 的角色。

简单解释一下我的工作范式。具体到每一条漏洞,流程大致是:

  1. Codex 先打铁:根据 CVE 描述和相关代码路径,产出第一版补丁。
  2. Claude Code 做 review:站在对抗视角挑毛病。
  3. 回到 Codex:我要求它,如果你同意 Claude Code 的意见,那就返工;如果不同意,那就反驳,把理由写清楚。
  4. 把所有思路摊开,再交给 Claude Code 做一轮 review。必要时来回再跑几轮,直到两边收敛。
  5. 进行测试:依然是类似的对抗操作,由 Codex 设计测试用例,Claude 补充。然后由 Codex 去实际执行并产出结果,再由 Claude Code review。
  6. 我来定夺:看 diff,跑测试,做最后决策与验收。

这个过程中,我自己不写一行代码。我的工作是定义问题、约束边界、挑方案、看 diff、跑验收、拍板。

公开提交页上,你能直接看到 VonngCodexClaude Code 三个名字同时出现在几条关键安全提交的 Co-authored-by 里。这不是作秀。这就是 2026 年一个人维护一个中型基础设施项目的真实样子。

fix.webp

这种协作模式有几个实际的好处。

第一,两个 agent 对抗能筛掉大部分“听起来都对、实际上不对”的方案。 单独一个 agent 在修复安全漏洞时会有一种 “幻觉级自信”,写出一份解释流畅、看起来干净的补丁,但漏掉了一个边界条件。让另一个 agent 从敌对视角审视它,这种方案很难活过第一轮。

第二,逼出显式的权衡。 两家不同实现路径撞上了,自然就要回答 “为什么你选 A 而我选 B”。这个对话本身就是在把隐性假设显性化,而显性化的假设,才是我作为 Blind Manager 能拍板的东西。

第三,真正的维护是“补丁打补丁”,而不是一把梭。 拿 LDAP STS 这条洞来说,首版修复推出来以后,很快发现成功请求不该消耗限流额度、默认不该信任 X-Forwarded-For、限流账户要按 “源 IP + 归一化用户名” 双维度算账。 然后又连着补了三次提交才算收敛干净。这个过程如果没有 agent 的火力支持,单个 maintainer 要一边读代码一边迭代,成本是完全不一样的。

agent.webp


有些事还是要人来拍板

但也正因为这个模式运转得不错,maintainer 唯一的不可替代性,就凸显在那些 AI 给不出最后答案的地方。

最典型的就是 OIDC 那条 fix。表面上,它是一个 JWT 算法混淆漏洞;但实质上,它是一个兼容性和安全性之间的取舍

简单解释一下。JWT 的签名算法分两类:非对称(RS256、ES256 这类,签名用私钥、验签用公钥)和对称(HS256 这类,签名和验签用的是同一个密钥)。OIDC 的标准姿势是 IdP 用自己的私钥签 token、MinIO 用公开的 JWKS 拿到公钥来验签。公钥是公开的,攻击者拿不到私钥,所以没法伪造。

而 HS256 这类对称算法的问题在于:签名和验签用的是同一个密钥。这个密钥就是 MinIO 自己也存着的 ClientSecret。于是攻击者只要拿到这个 “共享秘密”,就既能当裁判又能当运动员。自己用它签一张 token,MinIO 拿自己存的同一个密钥一验,当然通过。

这在教科书上是 JWT 的经典反模式,但历史代码就是这么走过来的。修法有几条路可选:

  • 继续容忍这条历史路径,只在某些条件下收窄:保留向后兼容,但安全边界依旧模糊。
  • 严格 JWKS-only,拒绝 HS256 等对称签名 token:一刀切、安全边界清晰,但少数本来就配得模糊的用户会感到配置失效。

两个 agent 可以给我列出每个方案的 trade-off,可以写好任何一个方案对应的补丁,但它们不会替我决定。最后我的选择是后者,恢复严格的 JWKS-only 验证路径,明确拒绝不该接受的 HS256。

这个决定也许会让少数模糊配置失效,但安全边界终于清楚了。AI 可以提三个方案,真正承担后果的人还是 maintainer

这就是 Blind Manager 模式的上限,也是下限:机器负责穷尽方案,人负责选择方向。


不是情怀,是必需

我一直说,这个 fork 不是情怀,也不是 cosplay。它存在,首先是因为这是我自己要用的东西。

MinIO 是 Pigsty 的生产依赖。我需要可用的二进制、完整的控制台、持续可得的包,以及真正有人处理的 CVE 补丁。也正因为我自己在用,所以这条线没有太多空话空间:它不是拿来讲故事的,而是拿来顶生产环境的。

这也决定了我的策略很保守。不会去追求 “新特性很酷”,也不会把仓库弄成另一个方向的实验场。我的目标一直都很明确:保持兼容,守住供应链,在该修的时候把问题修掉。

到现在,这个分支在 GitHub 已经有了 1300 star,在 Docker Hub 也累计了 五万+ 下载。数字本身不算什么惊人的成就,但它至少说明了一件事:需要这条线的人,并不只有我自己。

credit.webp

对已经在用 MinIO 开源版的人来说,迁移到这个分支的成本其实很低:

pigsty-module.webp

你不需要换掉整个系统,也不需要重新学习一套对象存储;多数情况下,只是把一个失去维护的上游,替换成一个还会继续交付补丁的分支。 如果你需要完整的生产级部署方案,Pigsty 里也提供了开源免费、开箱即用的 MinIO 生产级高可用部署支持。


承诺是什么

两个月前那篇文章发出去以后,有人私信我,说这事看着挺悲壮。其实不是。

写那篇文章的时候我没有赌气,发这个版本的时候我也没有激动。从头到尾,这就是一件普通得不能再普通的事,用的东西坏了,自己修一下。仅此而已。

只是到了 2026 年,“自己修一下” 这件事的门槛,被 AI Coding Agent 重新定义了。一个人,加两个 agent,加一点点判断力,足以把一个六万 star 的中型基础设施顶起来。这不是我厉害,这是时代变了

以前我们谈论开源的韧性,谈的是 “社区”,几十上百个志愿者众筹时间。现在这套剧本还在,但底下多了一层保险:哪怕社区散了,只要有一个人还愿意按下 fork 按钮,项目就能续命。

承诺是什么?承诺不是 “我有激情”,也不是 “我有道义”。承诺是 “下一个 CVE 出来的时候,我还在”

releasenote.webp

下一个 CVE 出来的时候,老冯还在。

就这样。

MinIO 已死,MinIO 复生

MinIO 开源仓库正式归档,不再维护。一个时代落幕,但开源的精神不死。 老冯 Fork 了 MinIO,复活了管理控制台,重建了二进制分发渠道,让 MinIO 浴火重生。

如果你正在用 MinIO,把 minio/minio 换成 pgsty/minio ,其他一切照旧。


MinIO 的死亡证明

2025年12月3日,MinIO 在 GitHub 上宣布进入"维护模式"。我写了一篇《MinIO 已死》。

2026年2月12日,MinIO 在 GitHub 首页将状态从"维护模式"更新为 “不再维护”,随后正式将仓库归档(Archived)。Read-only,不接受 PR Issue,不接受任何贡献。 一个拥有六万 star、超过十亿次 Docker 拉取的项目,变成了一座数字墓碑。

archived.webp

如果说12月是 临床死亡,那 2月的这个提交就是 正式开具了死亡证明

今天(2月14日),一篇题为《How MinIO went from open source darling to cautionary tale》的长文引发了广泛传播,详细复盘了 MinIO 从开源宠儿到反面教材的完整堕落时间线。

mermaid-timeline.webp

Percona 创始人 Peter Zaitsev 也在 LinkedIn 上表达了对开源基础设施可持续性的忧虑。国际社区的共识已经形成:MinIO 完了

peter.webp

不是 “不更新了” —— 是 彻底的、不可逆的、官方盖棺定论的死了

回顾这18个月的时间线,你会发现这不是一次意外死亡,而是一场蓄意的、分阶段的自毁:

时间事件性质
2021-05Apache 2.0 → AGPL v3许可证武器化
2022-07公开攻击 Nutanix许可证执法
2023-03公开攻击 Weka许可证执法
2025-05阉割管理控制台功能阉割
2025-10停止分发二进制/Docker断供
2025-12宣布维护模式临终关怀
2026-02仓库归档,不再维护死亡

一家融了1.26亿美元、估值十亿美金的公司,花了五年时间,亲手把自己建立的开源生态一砖一瓦地拆干净。

这比跑路还让人难受 —— 因为跑路至少是一次性的,MinIO 选择了凌迟。


但开源不死

故事到这里,按照正常剧本应该是一声叹息,然后大家各回各家。

但我想讲一个不一样的故事 —— 不是悼词,是复活

MinIO 公司可以归档一个仓库,但它归档不了 AGPL 协议赋予社区的权利。

讽刺的是,AGPL 正是 MinIO 自己选的。他们当年从 Apache 2.0 换成 AGPL,是为了在保留开源名份的同时拿它当武器打 Nutanix 和 Weka。 但开源许可证 是双刃剑 —— 同一把刀,如今也 保障了社区 Fork 的完全合法性。 代码一旦以 AGPL 发布,许可就不可撤回。你可以把仓库设为只读,但你收不回已经发出的许可证。

这就是开源协议设计的深意:公司可以抛弃项目,但不能带走代码。

所以 —— MinIO 已死,但 MinIO 也可以复生。

但也先别急着热血沸腾。Fork 谁都会,点一下 Fork 按钮的事。 真正关键的问题不是 “能不能 Fork”,而是 有没有人真的能把它当成生产组件来维护。


我本来并不想接这个摊子 —— 但我在 MinIO 进入维护模式后等了一两周,社区里没有人站出来说 “我来”,我就只能自己上了。

简单介绍一下背景:我一个人维护着整个 Pigsty 项目 —— 一个全功能的 PostgreSQL 发行版,451 个扩展,支持 14 个 Linux 发行版的交叉构建。 我同时维护着 270+ PG 扩展、六七款 PG Fork、几十款 Go 软件(Victoria/Prometheus 等)的全平台构建工作流,还是游刃有余的。

我对 MinIO 也不陌生。2018年,我们在探探内部就维护过一个 MinIO 的内部分支(当时还是 Apache 2.0), 支撑了约 25 PB 数据,是当时国内最早、最大的 MinIO 部署之一。

更关键的是,MinIO 在 Pigsty 中是 真实使用的组件, 很多用户将它作为 PostgreSQL 的备份仓库默认跑在生产环境里。

minio-doc.webp

这不是一个 “要不要做” 的问题,而是 不做不行。 早在2025年12月 MinIO 宣布维护模式时,我就已经自己动手创建了修复了 CVE 的二进制。

releases.webp

pgsty/minio RELEASE.2025-12-03T12-00-00Z


我们做了什么

截至今天,我们做了三件事。

1. 复活管理控制台

这是社区最愤怒的一刀。

2025年5月,MinIO 把完整的管理控制台(Admin Console)从社区版中移除,只留下了一个残废的对象浏览器。 用户管理、桶策略、权限配置、生命周期管理…… 一夜之间全没了。想要?掏钱买企业版。

我们把它弄回来了。

gui.webp

讽刺的是,这甚至不需要逆向工程。你只需要把 minio/console 子模块的版本号改回去就行了。 也就是说,MinIO 当初做的事情就是 改了一个依赖版本号,把完整控制台换成了残废版。功能都在那,代码都在那,他们只是给你关上了门。

console.webp

他们拆了门窗,我们给装回去了。

2. 重建二进制分发

2025年10月,MinIO 停止分发预编译的二进制文件和 Docker 镜像,只留源码。“请用 go install 自己编译” —— 这是他们给用户的交代。

对于绝大多数用户来说,开源软件的价值不只是一份源码副本 —— 供应链的稳定性才是命脉。 你需要的是一个可以写进 Dockerfile、放进 Ansible Playbook、塞进 CI/CD Pipeline 的稳定制成品,而不是每次部署前先装个 Go 编译器。

我们重建了完整的分发渠道:

Docker 镜像
pgsty/minio 已上线 Docker Hub,docker pull pgsty/minio 即可使用
RPM / DEB 包
为主流 Linux 发行版构建了与原版规格一致的安装包。
CI/CD Pipeline
GitHub 上全自动化构建流程已经搭建完毕,确保供应链持续稳定。

如果你在用 Docker 镜像,把 minio/minio 简单换成 pgsty/minio 就好了

喜欢原生 Linux 安装的朋友,可以直接从 GitHub Release 页面下载 RPM/DEB 包。 老冯的 pig (PG扩展包管理器)也可以简单的免翻墙安装。你也可以自己配置启用 pigsty-infra APT/DNF 软件仓库来安装。

curl https://repo.pigsty.cc/pig | bash; 
pig repo set; pig install minio

一切照旧。

3. 复活社区版本文档

MinIO 的官方文档同样面临风险 —— 原本的链接已指向它们的商业产品 AIStor。

所以我们基于 minio/docs 进行了 Fork,修复了失效链接,恢复了被删除的控制台文档,部署在:https://silo.pigsty.io

文档采用与原版相同的 Creative Commons Attribution 4.0 协议,完整保留了所有内容,并持续进行必要的维护更新。

doc.webp


我们的承诺与原则

一些话需要提前说清楚,免得产生误解。

我们不做新特性,只保障供应链

MinIO 作为一个 S3 兼容的对象存储,功能已经足够完善。 它是一个已经完成的软件,它不需要更多花里胡哨的新特性,它需要的是一个稳定可靠、持续可用的版本。

我们做的核心事情就是:确保你随时可以拿到一个能用的、完整的、带管理界面的 MinIO 二进制制成品。 RPM、DEB、Docker 镜像 —— CI/CD Pipeline 自动构建,与你现有的基础设施无缝对接。 不用担心某天 docker pull 拉不到镜像,不用担心 yum install 找不到包。

前提是 MinIO 别用商标武器来搞我,搞我那我就只能重命名了。

这是真实使用的版本,不是归档备份

可能有人会想:这只是又一个 Fork 备份而已吧?不是。 MinIO 在 Pigsty 中是真实使用的组件,很多用户将它作为备份仓库跑在生产环境里。 我们使用的就是自己构建的版本 —— 如果出了问题,我们会第一时间发现,第一时间修复。 我们自己构建的版本,已经在自己的生产环境中用了三个月。吃自己的狗粮,是最好的质量保证。

我们会修 Bug 并跟进安全更新

如果你在使用中遇到问题,欢迎在 pgsty/minio 提交反馈。 如果是我们构建的版本中可复现的问题,以及安全漏洞(CVE),我们都会积极跟进和修补 —— 但请不要将此视作商业 SLA 承诺 —— 我们尽最大努力,以开源社区的方式运作。

在 AI 编码能力突飞猛进,以及决定不做新特性的前提下,我认为只是修复 BUG/漏洞的工作量是完全可控且可以接受的。

商标问题很难搞,但走一步算一步

商标声明:MinIO® 是 MinIO, Inc. 的注册商标。 本项目(pgsty/minio)为社区独立维护的 AGPL 开源 Fork, 与 MinIO, Inc. 无任何关联、从属或背书关系。 本文中对 “MinIO” 的使用仅用于指代该开源软件项目本身,不暗示任何商业关联。

AGPLv3 虽然允许我们合法 Fork 和分发,但商标法是另一个领域。 虽然我们已经在所有地方明确标注了这是一个独立的社区维护版本, 但 MinIO 公司可能会以商标侵权为由对我们提出异议,要求我们停止使用 “MinIO” 这个名字。

如果 MinIO 方面对商标使用提出异议,我们会配合更名。(大概会叫 silo, stow 之类的) 但在此之前,我们认为在 AGPL Fork 中描述性使用原项目名称是合理的, 毕竟我们也不想把所有的 minio 给重命名了 —— 这对用户没有任何帮助。

AI 改变了游戏规则

可能有人会问:一个人能维护得了吗?

2026 年了,情况和五年前不一样。AI 编码工具正在改变开源维护的经济学

一个复杂 Go 项目的 bug 定位和修复,在 Claude Code 的辅助下,成本已经降低了不止一个数量级。 以前维护一个复杂基础设施项目需要一个专业团队,现在一个自带 AI 助理的老司机就够了。

你想,马斯克砍到 30 人的工程团队就能维护 X/推特 这种级别的系统。 维护个 MinIO 真没什么大不了的 —— 你只需要有测试验收能力就够了。

老冯行,老冯自己就上了。


Just Fork it!

MinIO 公司可以归档一个 GitHub 仓库,但它归档不了六万颗 Star 背后的需求, 归档不了十亿次 Docker Pull 背后的依赖。这些需求不会消失,它们只会寻找新的出口。

HashiCorp 的 Terraform 被 Fork 成了 OpenTofu,活得好好的。 MinIO 的情况其实更有利 —— AGPL 比 BSL 更友好,社区 Fork 没有任何法律风险。 公司可以抛弃项目,但开源协议的设计就不允许代码死掉。

git clone 是开源世界最强大的魔法。当一个公司决定关门的时候,社区只需要两个字:

Fork it.


参考阅读

MinIO已死,谁能接盘?

前天 MinIO 宣告进入维护模式,老冯写了一篇《MinIO 已死》聊了聊这个话题。 很多朋友也问我,MinIO 既然摆烂躺平了,有谁能接 MinIO 的班?

大方向的话,台面上的替代品无非就那几个:Ceph、RustFS、SeaweedFS、Garage…… 老冯把这些方案都打好了 Linux 上的 RPM/DEB 包,挨个试了一遍。

总结一句话:没有完美替代

各有各的问题 —— Ceph 功能全但太复杂;SeaweedFS 针对小文件优化但需要独立元数据库;Garage 小巧玲珑但功能简陋;RustFS 兼容 MinIO 但竟然还是 Alpha。

MinIO 替代品速览

MinIO 是 AWS S3 的开源替代,所以从单纯的 对象 CRUD 功能 上来讲,任何兼容 S3 API 的对象存储系统都可以作为 MinIO 的替代品。 但如果考虑到非功能类特性 —— 可靠性,可运维性,复杂度,工具链,生态成熟度,运维 SOP 这些,想要 “平替” 掉 MinIO 确实不容易。

这里我们不聊商业存储,云厂商的对象存储服务,只聊开源项目的话,大体上会有这些选择:

Ceph 可能是企业用户的最佳选择,但学习曲线陡峭,适合有专人运维的团队,不像 MinIO 一个二进制走天下。 很多用户并不需要分布式块存储和分布式文件系统的功能,而且运行还需要额外的 Podmon,不如 MinIO 爽利。

SeaweedFS 质量不错,针对海量小文件场景做优化,O(1) 磁盘寻址,小文件场景性能碾压。 但它需要一个独立的元数据库来存储文件元信息,这就带来了外部依赖。如果你需要一个"通用对象存储",它不是最佳答案。

Garage 是欧洲 Deuxfleurs 团队的作品,拿过欧盟 NGI 资助,适合自托管爱好者和边缘计算场景。 非常轻量(10MB),但 S3 兼容性太弱,没有版本控制,跨区域复制,IAM 这些,不适合企业场景。

RustFS 是唯一一个瞄准"MinIO 替代"生态位的项目,但成熟度不足 —— 竟然还是 Alpha。

RustFS 是否可以替代 MinIO

在所有号称"MinIO 开源替代"的项目中,老冯本来最看好 RustFS,所以特地花了些时间测试。 我在 Pigsty 中尝试将 MinIO 换成 RustFS,大部分逻辑可以复用,但还是有些区别:

  • RustFS 对证书名称有特殊要求
  • RustFS 的健康检查接口与 MinIO 有所不同
  • RustFS 不支持 mc admin 管理命令,无法配置详细的 IAM 策略。这一点对企业用户而言比较重要。

总的来说,跑起来了,但老冯思考再三,还是把这个分支给放弃掉了。因为把 Alpha 版的软件用在生产环境实在是太不像话了。 我是比较期待 RustFS GA 版本出来之后,再来做一次评测。

RustFS 是否会重蹈 MinIO 覆辙

当然,RustFS 这个项目虽然看上去很有潜力,但也存在一些问题。例如,RustFS 是否会重蹈 MinIO 覆辙? 特别是,RustFS 在很多雷点上,跟 MinIO 十分相似。

老冯请 AI SOTA 三件套(GPT5-pro, Claude4-Opus, Gemini3-pro)对 RustFS 的风险进行了全面的分析评测。

其中 Gemini 对 RustFS 项目提出来了几项相当严重的指控,老冯又请 Claude 核实了一遍。

RustFS 这些风险信号与当年的 MinIO 几乎一模一样:Apache 2.0 + 版权转让型 CLA + 单一商业公司控制。 考虑到这些因素,老冯对 RustFS 的评级从 “乐观期待”,下调为 “谨慎观望”。


所以,应该怎么做?

老冯的 PostgreSQL 发行版 Pigsty 里面集成了 MinIO 作为对象存储的解决方案。 这完全是一个可选的模块,主要作用是 —— 存储 PostgreSQL 备份,以及在其他业务软件需要对象存储的时候提供一个 —— 比如自建 Supabase 。

考察了现有的生态替代品之后,老冯确实是不想再折腾换 MinIO 的事情了,也许会提供一个用 pgbackrest 自己的备份服务器替代掉 MinIO 的选项。

老冯觉得目前最优的方案,还是继续使用 MinIO 的最新版本,锁定版本,做好网络隔离。 等待半年左右,看看社区生态的发展再做打算。如果那时候 MinIO 有人接手,或者 RustFS GA 可用了,再做调整也不迟。

当然,RustFS 也可以抓住这个机会,抢占 MinIO 的生态位,并真的实现一个更好,更安全,协议更友善的 MinIO 版本。 机会不等人,老冯觉得这个窗口也就几个月时间,错过了就是错过了。


继续使用 MinIO 的注意事项

如果要继续使用 MinIO,有这么几个注意事项。第一是应该用什么版本。 虽然说,MinIO 20250422 版本是最后一个功能完整(带有控制台 GUI)的版本,但老冯还是建议使用最新的版本。

因为从 20250422 到现在(2025-12-08)这段期间,MinIO 有一个比较严重的 CVE 安全漏洞。

CVE-2025-62506: Privilege escalation via session policy bypass (HIGH)

这个漏洞允许低权限用户创建一个新的账户实现权限提升。不过如果是在内网环境中,并且做好网络隔离的话,这个漏洞的风险也是相对可控的。 这个漏洞已经在 MinIO 20251015 版本中修复了,但是 MinIO 很鸡贼的从这个版本开始移除了二进制,只提供源代码。

不过老冯觉得还好,因为 MinIO 是一个 Go 语言项目,编译就一条命令,跨平台编译 goreleaser 一把梭也很简单。 流程我已经跑通了,其实很简单,老冯就直接 Fork 了 MinIO :然后用 MinIO 自己的打包器做了 2025-12-03 的 RPM/DEB 包,起码不会带病上岗。 把 MinIO 重新从一个 “源码发行版” 恢复成一个二进制发行版。https://github.com/pgsty/minio

minio.png

不过,安全漏洞和BUG还是得有人来修的,MinIO 自己说还是会看情况修安全漏洞。 老冯觉得,社区里如果有人愿意接手 MinIO 的话,现在还真是一个非常好的机会。 从 20250422 版本作为基础,CherryPick 重要的 Bug 和安全修复的话,然后开始维护一个 MinIO 社区版本。

说到底,MinIO 经过这么长时间的社区打磨,已经是一个相当成熟稳定的对象存储系统了 —— 基本上算是一个 “已完成的软件”。 需要的不是更新跟进S3各种花里胡哨的新功能(S3 Vector/S3 Table),而是扎扎实实的修 Bug 和安全漏洞。

这种维护状态的软件,搞一个 LTS,社区自发维护起来并不难。如果 MinIO 团队不愿意继续维护,老冯觉得社区里会自发涌现出接棒的人选 —— 毕竟现在有很多商业存储硬件公司都在用 MinIO。 比起自己从零瞎搞一个对象存储,接手一个成熟的项目,反而是更省事的选择。

题外话与更新(2026-02-14),MinIO 官方仓库已经彻底归档并不再维护。 我创建了一个 Fork:pgsty/minio / 文档 https://silo.pigsty.io。 搭建了 CI/CD 提供 RPM/DEB 二进制包与 Docker 镜像,基于最后的上游版本 2025-12-03 构建,恢复了 2025-05 阉割的控制台能力。

MinIO已死

2025年12月3日,是个值得在开源软件历史上记一笔的日子。 MinIO 官方在 GitHub 上更新项目状态,宣布 MinIO 开源项目进入“维护模式” 。 这基本上宣告了 MinIO 作为一个开源项目的死亡。

MinIO 这家公司,终于完成了从“屠龙少年”到“恶龙”的华丽转身。

maintenance-mode.png


从屠龙勇者到新的恶龙

民主化时代(2014–2019):对象存储的 Apache

MinIO 成立于2014年,其创始愿景极具理想主义色彩——做“对象存储领域的 Apache”。在那个 AWS S3 统治云存储的年代,MinIO 以其极致的轻量化(单个静态二进制文件)和对 S3 API 的 100% 兼容性,迅速赢得了开发者的青睐。

在这一阶段,MinIO 采用宽松的 Apache 2.0 许可证,鼓励开发者将其集成到各种应用中。其核心价值主张是“让任何硬件都能变成 AWS S3”。这种开放策略极其成功,MinIO 官方宣称其 Docker 镜像下载量超过10亿次,成为全球部署最广泛的对象存储服务 。此时的 MinIO 是云原生技术栈的宠儿,是 Kubernetes 环境中标配的存储后端。

许可证武器化(2019-2025):AGPL 攻防战

社区关系的第一次重大裂痕出现在2019年至2021年间。MinIO 宣布将其核心许可证从 Apache 2.0 变更为 GNU AGPLv3 。

虽然官方解释称此举是为了防止云厂商(如 AWS、Azure)“白嫖”代码并将其包装为专有服务——这是开源界常见的防御性手段。 这一时期,MinIO 从社区的守护者转变为激进的知识产权捍卫者。 2022 年, MinIO 公开指责 Nutanix Objects 产品侵犯其许可证,撤销了Nutanix 的使用授权;2023 年, MinIO 以类似理由起诉高性能文件系统厂商 Weka。 这些法律行动虽然在法理上具有争议,但释放了一个明确的信号:MinIO 不再欢迎未经付费的商业集成。这为2025年的全面封锁奠定了法律和心理基础。

阉割控制平面(2025-05)

2025年5月,当时 MinIO 决定从社区版代码中移除 MinIO Console——这是一个集成了存储桶管理、身份与访问管理(IAM)、监控和日志审计的关键图形用户界面(GUI)。 此次剥离后,开源版 MinIO 仅剩下一个基础的“对象浏览器”,仅具备查看和下载文件的能力。

而身份策略管理,站点复制配置,生命周期管理等核心运维功能被完全移至商业企业版。 这一变更将社区版 MinIO 从一个功能完备的存储管理系统降级为一个单纯的数据平面组件,剥夺了其作为独立产品在生产环境中使用的控制平面能力

中断二进制分发(2025-10)

2025年10月15日,当时,正值一个关键安全漏洞(CVE-2025-10-15T17-29-55Z / GHSA-jjjj-jwhf-8rgr)被披露之际,MinIO 停止了向 Docker Hub 和 Quay.io 发布更新的 Docker 镜像 。 这一时间点的选择具有极高的战略意味。在重大安全漏洞爆发期间切断二进制分发,实际上是将安全性变成了一种“勒索”筹码。

这一决策直接切断了绝大多数企业级用户的自动化部署链路,使得依赖 docker pull minio/miniohelm install 的标准 CI/CD 流程瞬间失效。 对于那些缺乏 Go 语言编译环境或内部容器镜像仓库维护能力的团队而言,这实际上等同于不可用。

维护模式(2025-12)

2025年12月3日,MinIO, Inc. 在其官方渠道及 GitHub 仓库中正式更新了项目状态,宣布 MinIO 开源项目进入“维护模式”。 README 上写到:以后不再提供功能更新改进,不再审Issue合 PR,重大安全问题看情况。 不再提供 RPM/DEB 包与 Docker 镜像,不再加新功能,需要维护的企业用户请切换到商业版本 AIStor 上。

aistor.png


技术影响:对开源生态的破坏

MinIO 进入维护模式对现有技术栈造成了即时且深远的破坏。

CI/CD 管道的断裂与自动化危机

成千上万的 Helm Charts、Ansible Playbooks 和 Terraform 脚本依赖于 minio/miniobitnami/minio 镜像。 随着官方停止发布镜像,Bitnami 等第三方打包商也因无法获取上游稳定代码而被迫停止更新 。

  • 连锁反应: 新环境的部署将直接失败;自动扩缩容组(Auto-scaling groups)在拉取新节点时会因找不到镜像而挂起。
  • 修复成本: 企业必须重写所有的部署脚本,指向私有镜像仓库,并建立内部的构建流程来从源码编译 MinIO。

安全真空:CVE 管理的私有化

停止发布二进制文件最致命的后果是安全补丁的滞后。以2025年10月的漏洞为例,MinIO 实际上扣留了二进制补丁 。

  • 风险暴露: 缺乏专门安全团队的中小企业将被迫继续运行含有高危漏洞的旧版本。
  • 合规噩梦: 对于受 PCI-DSS、HIPAA 或 SOC2 监管的企业,无法获得供应商签名的安全更新意味着合规性失效。

运维复杂度的指数级上升

UI 的移除不仅是用户体验的倒退,更是运维成本的增加。 过去只需在 Console 中点击几下即可完成的存储桶策略配置、用户权限分配,现在需要运维人员熟练掌握 mc 命令行工具或编写复杂的 JSON 策略文件。 这无形中提高了使用门槛,使得 MinIO 不再适合作为轻量级的内部工具使用。


背后的原因:资本与商业化的压力

MinIO 的技术决策根本动力来自于资本市场的估值逻辑。截至2025年,MinIO 已累计融资1.26亿美元。 其中最具决定性的是2022年1月完成的1.03亿美元 B 轮融资,由英特尔资本(Intel Capital)、软银愿景基金2期和 General Catalyst 领投。 这轮融资将 MinIO 推上了10亿美元估值的“独角兽”宝座。

在风投逻辑中,10亿美元的估值意味着公司必须展现出通往IPO的明确路径,通常要求年经常性收入(ARR)达到1亿美元以上,并保持高速增长。 2025年2月,MinIO 宣布其 ARR 在过去两年增长了149% 。虽然增速可观,但要支撑如此高的估值,仅靠自然转化已不足够。

停止开源支持,是将庞大的用户基数强制转化为付费客户的最直接手段。

2025年,MinIO 进行了全面的品牌重塑,推出了“MinIO AIStor”,将自己定位为“企业 AI 的数据基石”。 公司管理层意识到,通用对象存储(用于备份、网盘)的市场已是一片红海,且利润微薄;而生成式 AI(Generative AI)对高性能数据吞吐的需求(Exascale AI)才是下一个增长爆发点。 通过优化 AI 工作负载并专注于服务财富500强企业 ,MinIO 实际上决定剥离低价值的开源用户群体。 维护模式的开启,标志着 MinIO 正式从一个广泛的开源项目转型为一家服务于高端 AI 客户的垂直软件供应商

MinIO 已经不是几个极客在车库里写的玩具了,它是一家融资了 1.26 亿美元、估值超过 10 亿美金 的商业公司。 它的背后站着 Intel Capital,站着 软银愿景基金当你拿了风投那么多钱,你的老板就不是用户,而是投资人。 投资人要的是什么?是 ARR(年度经常性收入),是 增长率,是 IPO。 你跟投资人说:“我有10亿次 Docker 下载量!” 投资人会问:“这10亿次下载,给你付了一毛钱吗?”

现实就是这么残酷。那帮用免费版 MinIO 的中小企业、个人开发者,在资本眼里就是低价值资产。 你们提 Issue 报 Bug,群里问东问西,消耗的是昂贵的工程师工时,带宽和服务器资源,而你们 永远不会转化为付费客户。 MinIO 的管理层很清楚,他们的真正金主是那些搞 AI 大模型 的 500 强企业。 那些训练 GPT、跑自动驾驶数据的公司,需要的是 AIStor,是极致的性能,是 7x24 小时的 SLA 。

所以,开启“维护模式”,本质上是一次资产剥离。MinIO 决定切掉这块“坏肉”(免费用户),把所有资源集中到能产奶的“金牛”(企业级 AI 客户)身上。 从商业策略上讲,这叫聚焦。 从对投资人的交代上讲,这叫负责。 只是从开源上来说,这叫缺德


老冯的感想

老冯大概从 2018 年开始使用 MinIO ,当时还是 Apache 许可证,我们搞了几个几 PB 的对象存储,用来存放视频,图片,备份 —— 可能是那时候国内最大规模的部署实践。 老冯也编写了 MinIO 部署监控,扩缩容的 Playbook ,算是做过一些贡献 —— 现在还能在 Pigsty 中开源提供。

minio.png

作为开源创业者,老冯不是不能理解这种改变的动机。 但是站在开源贡献者与用户的立场 —— 老冯也知道很多兄弟现在心里只有一句话:“我从未见过如此厚颜无耻之人。”

开源协议虽然不是卖身契,但它是一种社会契约。 开发者贡献代码、用户贡献测试场景和口碑,大家一起把项目捧红。 MinIO 享受了十年的社区红利,靠着“全球下载量第一”的虚荣指标拿到了融资, 转头就对这就帮把它捧上去的用户说:“你们是搭便车的,滚蛋。” 这种行为破坏了开源社区最底层的信任。

这种“养套杀”的手段,比币圈的 Rug Pull 还要恶心。币圈割的是钱,MinIO 割的是全球数万家企业的技术栈 —— 用户的选择其实不只是一个二进制,而是一个软件生态和设计哲学。等大家都上车了,把迁移成本堆高到无法承受,然后突然抽走梯子。 这种模式,开源专家 Tison 在《诱导转向的伪开源战略》已经聊的很透聊。

诱导转向的核心问题在于 欺骗,既然 MinIO 背叛了社区,社区也会抛弃它。GarageSeaweedFS 甚至 RustFS,替代品有很多。江湖路远,后会无期。 如果要说老冯的感想是什么,那么就借用《银河系漫游指南》里海豚临走时说的那句话吧:

—— “So long, and thanks for all the fish.” —— 再见,多谢你们的鱼了。

题外话与更新(2026-02-14),MinIO 官方仓库已经彻底归档并不再维护。 我创建了一个 Fork:pgsty/minio / 文档 https://silo.pigsty.io。 搭建了 CI/CD 提供 RPM/DEB 二进制包与 Docker 镜像,基于最后的上游版本 2025-12-03 构建,恢复了 2025-05 阉割的控制台能力。

发布注记

SILO 各个正式版本的详细发布说明,按时间从新到旧排列。

每个 SILO 正式版本均有独立页面,记录发布日期、主要变更、安全修复、依赖更新与相关提交。

SILO 20260618 发布

加固 LDAP STS、补齐 S3 Select 记录限制、移除 ReadMultiple、升级 Go 1.26.4 并刷新安全依赖。

发布日期: 2026-06-18 · 版本: RELEASE.2026-06-18T00-00-00Z

这是 pgsty/minio 社区分支的一次例行安全与依赖维护更新。该版本加固 LDAP STS 限流,补齐 S3 Select 超大记录限制,移除废弃的 ReadMultiple 节点间 storage-REST API,将 Go 构建基线升级到 1.26.4,并刷新 Go 模块依赖以吸收第三方安全修复。

主要变更

  • 移除废弃的 ReadMultiple storage-REST API:旧的节点间 /rmpl 端点不再保留兼容路径,而是连同 route、handler、client wrapper、storage interface、xlStorage 方法、生成数据类型和相关 metric 一并移除。上游 multipart 处理迁移到 ReadParts 之后不应再有生产调用者,但滚动升级时仍应让集群运行一致版本。
  • 补齐 S3 Select 超大记录限制:JSON Lines 输入现在走有界 reader 路径,因此在支持 SIMD 的 CPU 上也会一致拒绝超大记录,不再绕过限制。S3 Select stream error 现在会保留预期错误码,并将 JSON parser 错误包装为 JSONParsingError
  • 加固 LDAP STS 限流源地址分桶:限流现在仅按源 IP 分桶,避免按用户名共享的 bucket 被单一客户端耗尽并锁定合法用户。受信代理处理现在从右到左解析 X-Forwarded-For,拒绝全网段 trusted-proxy CIDR,忽略 RFC 7239 Forwarded,并明确记录 X-Real-IP 的部署约定。
  • 刷新 Go 运行时与模块基线:release、hotfix、goreleaser 与 old-CPU Docker 构建现在都使用 golang:1.26.4-alpinego.mod 更新到 Go 1.26.4;依赖刷新覆盖 NATS、Prometheus、Azure SDK、Apache Thrift、gRPC、OpenTelemetry、Google API/auth、Go x/* 以及相关间接依赖。

直接安全修复

  • CVE-2026-42600:移除废弃的 ReadMultiple storage-REST API,关闭 /rmpl 暴露的旧节点间文件读取路径。
  • CVE-2026-39414:补齐 JSON Lines 输入的 S3 Select 超大记录限制,并保留正确的 S3 Select 错误语义。
  • CVE-2026-33419:进一步强化 LDAP STS 限流记账与 trusted-proxy 源 IP 处理。

依赖安全更新

  • github.com/Azure/go-ntlmsspv0.1.0 升级到 v0.1.1,修复 CVE-2026-32952,避免畸形 NTLM challenge 触发 Go 进程 panic。
  • github.com/apache/thriftv0.22.0 升级到 v0.23.0,修复 Go TFramedTransport 实现中的 CVE-2026-41602
  • github.com/nats-io/nats-server/v2v2.11.1 升级到 v2.11.15,吸收 NATS 2.11.x 安全补丁线。重点覆盖 pre-auth WebSocket 与 leafnode 拒绝服务、MQTT 授权、JetStream 管理 API 授权、凭据暴露和请求身份伪造等问题,包括 CVE-2026-27889CVE-2026-29785CVE-2026-33217CVE-2026-33218CVE-2026-33222CVE-2026-33247
  • github.com/prometheus/prometheusv0.310.0 升级到 v0.311.3,吸收 Prometheus 关于 remote-read 拒绝服务、Web UI 存储型 XSS、remote-write 配置 secret 暴露等安全修复,包括 CVE-2026-42154CVE-2026-44903CVE-2026-42151CVE-2026-40179
  • 将发布构建基线升级到 Go 1.26.4,并刷新 golang.org/x/cryptogolang.org/x/netgolang.org/x/sysgolang.org/x/textgoogle.golang.org/grpc 与 OpenTelemetry 等模块族。即便此前锁定版本已经越过特定公开 advisory 的修复范围,这些更新仍让社区分支继续贴近上游已修复依赖基线。
  • 5e40665:fix: harden LDAP STS rate-limit source bucketing
  • fd69c89:fix: complete CVE-2026-39414 S3 Select record limit enforcement
  • 73ac524:fix: CVE-2026-42600 remove ReadMultiple storage-REST API
  • df627ff:fix: bump Go toolchain to 1.26.4
  • 3e61b1d:chore: update Go module dependencies

SILO 20260417 发布

针对 OIDC、LDAP STS、S3 Select、复制元数据、unsigned-trailer 与 Go 工具链的集中安全加固。

发布日期: 2026-04-17 · 版本: RELEASE.2026-04-17T00-00-00Z

本次版本聚焦安全加固与兼容性收敛,集中修复了 OIDC、LDAP STS、S3 Select、对象复制元数据、unsigned-trailer、Snowball 上传链路,以及依赖与 Go 工具链相关的多项安全问题,并同步完成 LDAP TLS 回归修复与社区分叉文档整理。

主要变更

  • 身份认证链路收紧:OIDC / WebIdentity 现在只接受来自 IdP JWKS 的非对称签名 ID TokenHS256 等对称签名 token 不再被接受;LDAP STS 统一隐藏“未知用户”和“密码错误”的区别,降低用户名枚举风险。
  • LDAP STS 限流行为更新:限流现在同时按源 IP 与归一化用户名生效,成功请求不再错误消耗额度;默认仅使用 socket peer address 作为源地址,不再信任 X-Forwarded-ForX-Real-IPForwarded,如需按真实客户端 IP 限流,需显式配置 MINIO_IDENTITY_LDAP_STS_TRUSTED_PROXIES
  • 上传与写入路径更严格:presigned query 参数不能再与 unsigned-trailerPUT / multipart 上传组合使用;Snowball auto-extract 在 unsigned-trailer 路径下也会执行完整签名校验,匿名或伪造签名请求将被拒绝。
  • 复制元数据不再可伪造:普通 PUT / COPY 请求夹带的 X-Minio-Replication-* 内部复制头现在会被拒绝或忽略,只有可信复制链路才能写入相关内部元数据。
  • S3 Select 错误语义更明确:CSV 和 line-delimited JSON 遇到超大记录时会直接返回 OverMaxRecordSize,不再笼统返回 InternalError;依赖旧错误码的客户端或告警规则需要同步调整。
  • 运行时与依赖基线升级:修复 ldaps:// 未正确应用 TLS 配置的回归问题,替换 minio/pkg/v3pgsty/minio-pkg/v3,并锁定若干易引入 breaking changes 的关键依赖;同时升级 go-josego.opentelemetry.io 与 Go 1.26.2,统一构建与发布基线。
  • 文档与安全说明同步更新:刷新 SECURITY.mdVULNERABILITY_REPORT.mddocs/sts/ldap.md 等文档,新增安全通告索引,并将安全说明中的上游 minio/minio 引用统一切换为 pgsty/minio

修复的 CVE

  • CVE-2026-34986:升级 go-josev4.1.4,修复 JWT / JOSE 依赖中的已知安全问题。
  • CVE-2026-39883:升级 go.opentelemetry.io 依赖栈,修复 PATH hijacking 风险。
  • CVE-2026-33322:恢复严格的 JWKS-only OIDC JWT 验证路径,阻断 keyring 注入与算法混淆风险。
  • CVE-2026-33419:系统性强化 LDAP STS 认证、限流、源地址识别与记账逻辑,涉及 4 个后续修复提交。
  • CVE-2026-34204:拒绝不可信请求注入 X-Minio-Replication-* 元数据,防止对象被写入异常复制状态。
  • CVE-2026-39414:提前拒绝超大 S3 Select 记录,避免异常数据持续缓冲和解析。
  • GHSA-hv4r-mvr4-25vw:封堵 unsigned-trailer query auth 绕过。
  • GHSA-9c4q-hq6p-c237:加固 Snowball auto-extract 场景中的 unsigned-trailer 认证与签名校验。
  • CVE-2026-32280CVE-2026-32281CVE-2026-32283:升级 Go 到 1.26.2,吸收上游工具链与标准库安全修复。
  • c878ca0:fix: pin deps with breaking changes and fix LDAP TLS regression (#15)
  • e970ec5:fix: upgrade go-jose to v4.1.4 to patch CVE-2026-34986
  • a206510:fix: CVE-2026-39883 upgrade go.opentelemetry.io
  • fd65f11:merge: PR #18 upgrade go-jose to v4.1.4 for CVE-2026-34986
  • bc087e4:merge: PR #19 upgrade go.opentelemetry.io for CVE-2026-39883
  • f1f2239:fix: CVE-2026-33322 restore JWKS-only OIDC JWT verification
  • 6619d0c:fix: CVE-2026-33419 harden LDAP STS auth
  • fcb8f24:fix: CVE-2026-34204 reject untrusted replication metadata
  • c5765dc:fix: CVE-2026-39414 reject oversized S3 Select records
  • fa7c579:fix: GHSA-hv4r-mvr4-25vw block unsigned-trailer query auth bypass
  • b50ab58:fix: GHSA-9c4q-hq6p-c237 harden Snowball unsigned-trailer auth
  • 9a4b3cd:fix: CVE-2026-32280/CVE-2026-32281/CVE-2026-32283 upgrade Go to 1.26.2
  • c55b52c:fix: CVE-2026-33419 preserve LDAP STS rate limits on success
  • 817a457:fix: CVE-2026-33419 harden LDAP STS rate-limit source IP
  • 084a154:fix: CVE-2026-33419 tighten LDAP STS rate-limit accounting
  • 16e34f9:docs: refresh security guidance and fork references

SILO 20260325 发布

围绕打包、稳定性、LDAP TLS、Docker 镜像与依赖安全的维护版本。

发布日期: 2026-03-25 · 版本: RELEASE.2026-03-25T00-00-00Z

这是一个以打包、稳定性与安全公告为主的维护版本,重点是完善交付镜像、修复 LDAP TLS 回归,并把已经锁定到安全版本的依赖明确纳入发布说明。

主要变更

  • mcli/mc 打入 Docker 镜像并增加校验流程,改善镜像开箱体验。
  • 修复 ldaps:// 场景中的 LDAP TLS 回归,确保 TLS 配置能够正确透传。
  • 移除从上游继承但社区分支不再使用的 CI/CD 工作流,减轻维护负担。
  • 固定若干关键依赖,避免上游 breaking changes 继续向下游扩散。

修复的 CVE

  • CVE-2026-24051:发布说明明确将 go.opentelemetry.io/otel/sdk 锁定在安全版本 v1.42.0,规避 macOS 上因 PATH hijacking 导致的任意代码执行风险。
  • CVE-2025-10543:发布说明明确交付了 github.com/eclipse/paho.mqtt.golang v1.5.1,修复超长 UTF-8 字符串编码错误导致的 MQTT 数据包内容异常。
  • CVE-2025-58181:发布说明明确交付了 golang.org/x/crypto v0.49.0,修复 ssh GSSAPI 认证请求可触发的无界内存消耗问题。
  • f2f9a40:add mcli/mc from pgsty/mc to Docker image
  • ce1c537:fix: pin deps with breaking changes and fix LDAP TLS regression (#15)
  • ee55e53:remove upstream CI/CD workflows inherited from minio/minio

SILO 20260321 发布

升级 Go 1.26.1、适配更严格的编译与 lint 检查,并完成大范围安全依赖刷新。

发布日期: 2026-03-21 · 版本: RELEASE.2026-03-21T00-00-00Z

这是一次围绕 Go 1.26.1 与依赖收敛展开的维护版。除了适配更严格的编译与 lint 检查外,这个版本也完成了本轮最关键的一次安全依赖刷新。

主要变更

  • 将构建环境从 Go 1.26.0 升级到 Go 1.26.1
  • 全面刷新直接与间接依赖,收敛新工具链下的兼容性问题。
  • 修正 Go 1.26.1 更严格检查暴露出来的一批 lint 与测试问题。

修复的 CVE

  • CVE-2026-27137:Go stdlib 从 1.26.0 升级到 1.26.1,修复 crypto/x509 对邮箱约束校验不完整的问题。
  • CVE-2026-27138:Go stdlib 从 1.26.0 升级到 1.26.1,修复畸形证书触发 crypto/x509 panic 的问题。
  • CVE-2026-25679:Go stdlib 从 1.26.0 升级到 1.26.1,修复 net/url 对 IPv6 host literal 解析不严格的问题。
  • CVE-2026-27139:Go stdlib 从 1.26.0 升级到 1.26.1,修复 osFileInfo 可逃逸 Root 边界的问题。
  • CVE-2026-27142:Go stdlib 从 1.26.0 升级到 1.26.1,修复 html/templatemeta refresh 场景下 URL 未正确转义的 XSS 风险。
  • CVE-2026-26958filippo.io/edwards25519v1.1.0 升级到 v1.2.0,修复 MultiScalarMult 可能产生错误结果或未定义行为的问题。
  • CVE-2025-10543github.com/eclipse/paho.mqtt.golangv1.5.0 升级到 v1.5.1,修复超长 UTF-8 字符串编码错误导致的 MQTT 报文异常。
  • CVE-2026-24051go.opentelemetry.io/otel/sdkv1.38.0 升级到 v1.42.0,修复 macOS 上通过 PATH hijacking 触发任意代码执行的风险。
  • CVE-2026-33186google.golang.org/grpcv1.77.0 升级到 v1.79.3,修复 HTTP/2 :path 缺少前导斜杠时的授权绕过问题。
  • 5abd9a8:bump golang to 1.26.1 and update deps
  • 377fc61:fix: satisfy stricter Go 1.26.1 linter checks

SILO 20260314 发布

切换到社区维护的 Console 分支,并完成大范围兼容性与依赖刷新。

发布日期: 2026-03-14 · 版本: RELEASE.2026-03-14T12-00-00Z

这个版本完成了向社区维护 Console 分支的切换,并在切换过程中做了一轮较大幅度的依赖更新,为后续 Go 1.26.x 维护版打下基础。

主要变更

  • 切换到社区维护的 georgmangold/console v1.9.1,替换不可持续维护的上游 Console 依赖。
  • 大幅更新 go.mod 中的直接与间接依赖,使 Console 与新工具链组合能够稳定构建。
  • 修复 grid_test.gogo vet 格式化指令问题,并调整部分测试以适配 Go 1.26 的 HTTP 行为变化。

修复的 CVE

  • CVE-2025-47913golang.org/x/cryptov0.37.0 升级到 v0.46.0,修复 ssh/agent 在处理异常响应时可能导致客户端 panic 的问题。
  • CVE-2025-58181golang.org/x/cryptov0.37.0 升级到 v0.46.0,修复 ssh GSSAPI 认证请求可触发无界内存消耗的问题。
  • CVE-2025-47914golang.org/x/cryptov0.37.0 升级到 v0.46.0,修复 ssh/agent 对畸形身份消息缺乏边界校验导致的 panic 问题。
  • CVE-2025-47911golang.org/x/netv0.39.0 升级到 v0.48.0,修复 html.Parse 在特制输入下的二次复杂度解析问题。
  • CVE-2025-58190golang.org/x/netv0.39.0 升级到 v0.48.0,修复 golang.org/x/net/html 可能陷入无限解析循环的问题。
  • 68521b3:add github ci/cd pipeline
  • 00f3cf7:RELEASE.2026-03-14T12-00-00Z with go 1.26.0

SILO 20260214 发布

恢复内嵌 Console,建立 GitHub CI/CD,升级 Go 1.26.0,并补齐社区交付入口。

发布日期: 2026-02-14 · 版本: RELEASE.2026-02-14T12-00-00Z

这是社区分支早期的一个基础设施版本,除了恢复内嵌 Console 与建立 GitHub CI/CD 之外,也把 Go 运行时基线提升到 1.26.0,因此一并消化了上一代工具链中的一批安全问题。

主要变更

  • 恢复内嵌 Console,并更新 README 以明确社区维护分支的定位。
  • 新增 GitHub CI/CD 流水线,为后续自动构建和多平台分发建立基础。
  • 补充文档、Docker、GitHub 仓库与 pig 包管理器安装入口,完善发布入口。

修复的 CVE

这些问题随着 Go 从 1.25.5 升级到 1.26.0 一并被修复,包括:

  • CVE-2025-68121crypto/tls 在会话恢复时可能错误接受已变更 CA 配置。
  • CVE-2025-61730:TLS 1.3 在跨加密级别 record 中可能错误处理握手消息。
  • CVE-2025-61726net/url 查询参数解析可导致内存耗尽。
  • CVE-2025-61728archive/zip 构建索引时可能触发过高 CPU 消耗。
  • CVE-2025-68119cmd/go 在调用外部 VCS 工具链时可能触发意外代码执行。
  • CVE-2025-61731#cgo pkg-config: 指令可导致任意文件写入。
  • CVE-2025-61732cmd/cgo 文档注释解析差异可能导致代码走私。
  • 8630937:Restore embedded console and update README for community fork
  • 5d57938:add github ci/cd pipeline

SILO 20251203 发布

首个社区打包与分发基线,提供 APK、DEB 与 RPM 制品。

发布日期: 2025-12-15 · 版本: RELEASE.2025-12-03T12-00-00Z

这是目前可追溯到的首个社区版发布,主要目标是建立社区维护分支的打包与分发基线,而不是在此前社区版本之上做增量修复。

主要变更

  • 基于 minio/pkger 建立社区版打包流程。
  • 选择 MinIO 进入 maintenance mode 后的一版上游基线作为社区分支起点。
  • 首次产出 apkdebrpm 等多种平台包,为后续持续发布建立基础。

修复的 CVE

  • 这是首个社区版发布,GitHub Release 未单独声明相对更早社区版本的安全修复清单;本页不追溯其相对上游维护模式基线的全部历史 CVE 差异。
  • d4cd4b4:RELEASE.2025-12-03T12-00-00Z with go 1.25.5

SILO 安全编年史

SILO 分支处理过的应用层 CVE 编年史:按照时间顺序,每个 CVE 独立成篇。

这里记录 SILO 社区分支自分叉以来处理过的安全事件。文章严格按照发现与修复时间排列,每个 CVE 独立成篇:最初的威胁模型、复核中的转折、被否决的方案、最终恢复的不变量、验证证据与兼容性代价,都留在它自己的故事里。

编年表

日期CVE事件首个包含版本
2026-04-15CVE-2026-32285jsonparser:最终无需补丁的安全通告当前依赖图原本已经修复
2026-04-15CVE-2026-33322OIDC JWT 算法混淆SILO 2026-04-17
2026-04-15CVE-2026-33419LDAP STS 用户枚举与限流SILO 2026-04-17;2026-06-18 完整闭环
2026-04-15CVE-2026-34204复制元数据注入SILO 2026-04-17
2026-04-15CVE-2026-39414S3 Select 超大记录SILO 2026-04-17;2026-06-18 完整闭环
2026-04-16CVE-2026-40344Snowball 自动解包认证绕过SILO 2026-04-17
2026-04-16CVE-2026-41145Unsigned-Trailer 查询认证绕过SILO 2026-04-17
2026-06-12CVE-2026-42600ReadMultiple Storage-REST 路径穿越SILO 2026-06-18

下方文章按时间正序排列;同一天的事件按 CVE 编号排列。只涉及 Go 或依赖升级的 CVE 继续留在对应的发布注记中,不把它们包装成应用层漏洞故事。


CVE-2026-32285:最终无需补丁的 jsonparser 通告

一次没有代码改动的安全研判:当前依赖已经包含补丁,可达性检查也没有发现漏洞路径。

状态: 无需代码改动,问题已关闭
GitHub Issue: pgsty/minio#26

安全维护不只有“发现漏洞,然后提交补丁”。CVE-2026-32285 的初始判断是:仓库可能仍携带未修复的 jsonparser,甚至一度讨论过替换依赖或维护自己的分支。真正检查 module version 与可达性后,结论却是当前代码已经使用包含修复的 v1.1.2govulncheck 也没有发现可达的 vulnerable symbol。

最终正确的动作不是制造一个依赖升级,而是把证据记录下来,然后关闭问题。

初始判断为什么有问题

问题最初被理解成“该依赖没有可用的 fixed version”。如果直接按照这个前提行动,很容易出现几种看似积极、实际有害的结果:

  • 无意义地改变 dependency graph;
  • 为一次并不存在的修复引入新的兼容性回归;
  • 增加本地 fork 的长期维护负担;
  • 让用户误以为过去的 SILO Release 确实暴露于该漏洞。

安全工作不能用“有没有产生 diff”衡量。没有漏洞时不改代码,本身就是一个需要证据支持的安全决定。

研判过程

这次调查按四层证据逐步收敛:

  1. 核对当前 go.modgo.sum 中实际解析到的版本;
  2. 检查上游 release,确认 v1.1.2 已经包含对应补丁;
  3. 运行并核对 govulncheck,没有发现可达 symbol;
  4. 将 issue 中的差异归因于漏洞数据库或问题信息滞后,而不是当前源码仍然存在漏洞。

这里必须区分四件事:一个版本曾被标记为受影响、一个 package 被导入、漏洞 symbol 在程序中可达、以及远程输入能真正触发利用。这四个结论不能互相替代。

为什么不做“保险升级”

如果当前版本已经包含修复,再随便 bump 到另一个版本并不会让系统“更安全”。它只会扩大变化面,让后续回归更难归因。对一个庞大的 Go module graph 来说,这种无目标滚动尤其危险。

最终决定是:

  • 不提交假修复;
  • 在 issue 中保留版本与可达性证据;
  • 继续让版本 gate 与 govulncheck 防止未来依赖回退;
  • 不把“当前无需改动”写成永久豁免。

验证边界

本事件证明的是:2026-04-15 当时的 checkout 不需要为 CVE-2026-32285 修改代码。 它不代表所有未来分支、依赖图或发布版本永远不受影响。只要依赖发生降级或 module selection 改变,就必须重新做版本与可达性检查。

这篇文章记录的是当时的研判与关闭依据,不声称本次博客整理重新运行了原始 govulncheck

这次事件留下的原则

安全维护的目标是让风险结论准确,而不是让每个 issue 都产生代码。面对依赖型 CVE,最重要的是依次回答:当前到底解析到哪个版本、漏洞代码是否进入程序、symbol 是否可达、部署入口是否可利用。只有这些问题的答案要求改变源码时,才应该制造 diff。

CVE-2026-33322:OIDC JWT 算法混淆

OIDC verifier 混用 client secret 与 JWKS key,最终通过恢复 JWKS-only 非对称验证关闭算法混淆。

状态: 已发布
首个包含版本: RELEASE.2026-04-17T00-00-00Z
影响入口: AssumeRoleWithWebIdentityAssumeRoleWithClientGrants
GitHub Issue: pgsty/minio#22

旧实现把 OIDC client secret 放进 JWT verifier keyring,同时允许 HMAC signing method。知道 client secret 的攻击者因此可以自行签发 HS token,再通过 STS 换取临时权限。最终修复恢复了 JWKS-only 非对称验证:宁可明确打破 HS256/384/512 兼容,也不保留一个会重新混淆信任语义的开关。

漏洞不在“有没有验签”

表面上看,旧代码确实执行了 JWT signature verification。真正失效的边界是:verifier 接受了哪一种 key,以及 token header 是否能选择与该 key 不匹配的算法语义。

攻击链需要满足几个条件:

  • 攻击者获得 OIDC client secret;
  • 攻击者构造 HMAC-signed ID token;
  • verifier 把 client secret 当作 HMAC signing key;
  • token 进入 WebIdentity 或 ClientGrants STS flow,换取临时凭据。

client secret 泄露本身当然严重,但它原本不应该自动获得“签发任意用户 ID Token”的权限。把两种能力混在同一个 keyring 中,才是算法混淆的核心。

兼容路径写出来了,又被主动删除

修复过程中曾经实现过 allow_hmac 一类兼容路径。它看起来很合理:默认安全,确有需要的用户可以显式打开。但继续把 shared secret 放进通用 verifier keyring,意味着管理员需要理解这个配置实际上扩大了整个 STS 信任边界;未来 method allowlist 只要发生漂移,漏洞就可能重现。

几种方案的取舍最终非常清楚:

方案收益风险结论
保留 secret keyring,只限制部分算法改动小,兼容 HMAC IdPkeyring 仍混合两种信任语义否决
增加 allow_hmac 配置兼容性显式配置本身难以正确理解,测试面扩大实现后回滚
JWKS-only边界清晰,刷新与重试共用同一 parserHS 用户必须迁移接受

这次最重要的决策不是“新增了哪些代码”,而是主动删除了已经完成的兼容实现。

最终不变量

修复集中在 OIDC JWT 验证路径,并固定了四条规则:

  • verifier key 只来自 IdP JWKS;
  • OIDC client secret 不进入 JWT verification keyring;
  • HS256、HS384、HS512 一律拒绝;
  • 正常 RS256 流程以及 JWKS refresh/retry 使用同一 method allowlist。

修复没有借 CVE 顺手扩展 JOSE 功能。PS256 与 EdDSA 不在这次事件的支持范围内。

验证与发布

开发记录包含 HS256 rejection、RS256 acceptance、JWKS refresh/retry regression tests,以及 focused go test ./internal/config/identity/openid。临时兼容 helper、配置与测试在最终 diff 中全部删除。

公开发布 lineage 中的修复提交为 f1f2239,并随 SILO 2026-04-17 发布。这篇文章记录历史验证,不代表本次博客整理重新执行了测试。

兼容性代价

这是明确的 breaking change。仍签发 HS256/384/512 token 的 IdP 必须先迁移到 JWKS-backed RSA/ECDSA,再升级 SILO。这里选择的是更窄、更容易解释的信任模型,而不是让旧配置继续工作。

CVE-2026-33419:LDAP STS 用户枚举与限流链

从统一认证失败,到修正成功退款、代理来源与账户锁定:一次经历两轮反向修复的 LDAP STS 加固。

状态: 已发布,经历两轮后续修正
首个包含版本: RELEASE.2026-04-17T00-00-00Z
完整修正版本: RELEASE.2026-06-18T00-00-00Z
GitHub Issue: pgsty/minio#23

核心漏洞很直接:LDAP STS 对“用户不存在”和“密码错误”返回不同结果,形成 username oracle。第一版修复统一外部错误,并增加 source IP 与 username 双重限流;连续复核却发现,成功退款、可伪造来源 header、reservation accounting 和共享 username bucket 都可能让安全控制本身成为新的攻击面。

六月的最终方案删除了会造成精确账户锁定的 username bucket,只保留 source IP bucket,并把代理来源识别写成明确的部署契约。

初始威胁模型

入口是 AssumeRoleWithLDAPIdentity。攻击者不需要已有 MinIO 账号,只要能够访问 LDAP STS endpoint,就可以比较 unknown user 与 wrong password 的 code、status 或 message,逐步枚举有效用户名,再结合 password spraying、组织结构猜测或社工攻击。

修复也不能简单把所有错误都伪装成“密码错”。LDAP connection、lookup bind 或目录服务故障必须继续表现为基础设施错误,否则运维会失去诊断能力。

第一轮:统一响应并增加 limiter

2026-04-15 的初始修复做了三件事:

  • unknown user 与 bad password 对外返回同一 STS auth error;
  • LDAP infrastructure error 仍返回 500,并在 server log 保留真实原因;
  • 新增 in-memory limiter,最初同时按 source IP 与 normalized username 分桶。

这一版关闭了内容侧信道,也给暴力尝试增加了成本,但 limiter 的状态机与来源识别随后暴露出更多问题。

第二轮:成功、来源与会计

4 月 16 日的连续修正处理了三类缺陷:

  1. 成功认证不应消耗失败额度,reserve/commit/cancel/refund 生命周期必须明确;
  2. 默认只能使用 socket peer,不能直接信任 X-Forwarded-ForX-Real-IPForwarded
  3. refund 与 capacity 必须有边界,避免 cancel 逻辑凭空增发 token。

trusted proxy 需要显式 allowlist,而不是因为请求带着“真实 IP” header 就自动获得信任。

第三轮:删掉 username bucket

六月的对抗性复核推翻了“source + username 一定比 source-only 更强”的直觉。共享 username bucket 可以被任意来源持续耗尽,攻击者只需要低频请求就能在合法用户真正执行 LDAP bind 之前,精确锁死一个目标账户。

最终修复因此:

  • 删除 per-username bucket;
  • XFF 从右向左剥离 trusted hops,取第一个非可信地址;
  • 拒绝 0.0.0.0/0::/0 这类 trusted-proxy footgun;
  • Forwarded 不再用于安全敏感分桶;
  • X-Real-IP 只在代理覆盖而非透传客户端输入的契约下使用。

这次转折说明,安全控制必须拥有自己的威胁模型。限制更多维度,不等于更安全。

被否决的方案

方案否决原因
为未知用户执行 dummy bind放大 LDAP 压力并引入易错的第二条认证路径;内容侧信道已经关闭
IPv6 统一按 /64 分桶会让同一站点或运营商前缀下的合法用户互相误伤
XFF 直接取最左值客户端可伪造
XFF 与 X-Real-IP 不一致就回退 peer攻击者可故意制造不一致,把代理后的所有用户压入同一 bucket
完整支持 RFC 7239 Forwarded安全解析复杂度高,现实收益不足

验证与发布

历史记录覆盖 limiter reserve/commit/cancel/refund、并发、success、infra failure、unknown user/bad password 外部等价,以及 RemoteAddr、spoofed header、trusted proxy、多 hop 与 catch-all CIDR。focused package test 与 build 均有记录。

LDAP security e2e 在缺少 _MINIO_LDAP_TEST_SERVER 时会 skip,所以外层 ok 不能冒充真实 LDAP 全场景证明。

初始公开修复提交为 6619d0c,后续修正包括 c55b52c817a457084a1545e40665

最终代价与残余风险

  • limiter 最终只按 source IP,放弃跨来源的单账号 hard throttle;
  • limiter 是 per-node、in-memory,不是集群全局密码防护;
  • botnet、分布式来源、IPv6 地址轮换与 LDAP bind timing 仍然存在;
  • trusted proxy 配置错误仍会破坏来源归属;
  • Forwarded-only 部署会退化为 peer bucket,粒度更粗。

限流只能降低单一来源的尝试速率。真正隐藏 username existence 的,是统一的外部认证响应。

CVE-2026-34204:复制元数据注入

普通 PUT/COPY 可以伪造内部复制状态;修复让 replication-only metadata 只在授权复制路径中恢复。

状态: 已发布
首个包含版本: RELEASE.2026-04-17T00-00-00Z
GitHub Issue: pgsty/minio#24

普通 PUTCOPY 请求可以把 X-Minio-Replication-* header 伪装进内部 X-Minio-Internal-* SSE metadata,写出 replication state 与真实授权路径不一致、甚至无法读取的对象。最终修复不再默认接受 replication-only metadata,只在通过 ReplicateObjectAction 的可信复制流程中恢复,并在 CopyObject 的所有 header 消费者之前统一清洗。

威胁模型

攻击者只需要普通对象写权限,不需要 internode credential。输入完全来自客户端可控的 X-Minio-Replication-* header;metadata extraction 却会把它们转换成内部复制或 SSE 状态。

后续读路径按照错误的内部状态解释对象,可能导致对象不可读,形成完整性与可用性破坏。几乎所有接受不受信写请求的生产 server 都应该视为受影响。

问题的根本不是 header 名字本身,而是不可信来源的数据在没有经过复制授权的情况下获得了内部语义

全请求拒绝,还是精确清洗

看到 replication header 就拒绝普通请求,是最直观的修复。但这会把客户端过去可以携带的多余 header,从“被忽略”改变成 hard failure。最终选择了更精确的模型:

  • 默认 extraction path 不接受 replication-only metadata;
  • ordinary PUTCOPY 先剥离这些字段;
  • 只有通过 ReplicateObjectAction 授权后才恢复;
  • replica status 写入使用同一可信条件;
  • multipart 与 Snowball 的合法 replication flow 显式恢复所需的 SSE metadata。

这让兼容性变化停留在内部语义,而不是扩大到所有携带多余 header 的客户端。

为什么 CopyObject 必须提前清洗

CopyObject 的 header 不只用于最终 metadata map,还会提前参与 precondition 与 SSE-C source 处理。如果只在写入对象前删除,早期消费者已经被污染。

最终清洗发生在这些消费者之前,把“不可信 replication header 不进入内部语义”变成单一不变量,而不是依赖每个后续函数记得再检查一次。

实现与验证

改动覆盖 handler-utils、object handler 与 multipart handler,并加入了几层测试:

  • helper 层 trusted/untrusted metadata extraction;
  • handler 层 malicious PUTCOPY
  • CopyObject header sanitization;
  • vulnerable parent 与 patched tree 的红绿对照;
  • live server before/after,确认恶意 header 不再让对象不可读;
  • legitimate replication、multipart 与 Snowball flow 保持可用。

公开发布 lineage 的修复提交为 fcb8f24。这篇文章保留历史验证边界,本次博客整理没有重新启动 live server。

代价与残余风险

  • 普通客户端夹带的内部复制 header 现在会被忽略或清洗;
  • replication-only metadata 必须在授权分支显式恢复;
  • 未来新增复制入口如果忘记恢复,会表现为功能回归,而不是重新放开不受信写入;
  • 本次审计聚焦 replication header,不代表所有 X-Minio-Internal-* 字段都完成了同样的 trust audit。

这个事件留下的审查问题很简单:一个字段看起来像“内部字段”并不能证明它可信,必须继续追问它来自哪里,以及哪一个授权决定允许它获得内部含义。

CVE-2026-39414:S3 Select 超大记录与 SIMD 绕过

第一轮给 CSV 与 JSON Lines 加上 1 MiB 上限,第二轮又发现 SIMD fast path 完全绕过了它。

状态: 已发布,六月完成二次闭环
初始修复版本: RELEASE.2026-04-17T00-00-00Z
完整修复版本: RELEASE.2026-06-18T00-00-00Z
GitHub Issue: pgsty/minio#25

四月的第一轮修复使用既有的 1 MiB maxCharsPerRecord 同时限制 CSV 与普通 JSON Lines,避免在遇到分隔符前持续无界 buffering,并让客户端得到明确的 OverMaxRecordSize。六月复核却发现,支持 SIMD 的 CPU 会走另一条 simdjson fast path,完全绕过这个限制。

最终方案让 JSON Lines 统一走 bounded reader,同时修正错误码、parser error 与 terminal error 前的 completed-record flush。代价是暂时放弃 SIMD 快路径,以换取所有 CPU 上一致的安全语义。

威胁模型

攻击者可以提交或查询包含超长单条记录的对象。reader 在遇到 record delimiter 前持续缓存,造成 memory/CPU DoS。更麻烦的是,同一个输入会因为机器 CPU 能力不同而进入不同实现:测试机上的安全行为,并不一定等于生产机。

错误语义也属于修复的一部分。如果超大记录最后只表现为 generic InternalError,客户端与告警系统无法区分安全上限和服务端故障。

第一轮:复用已有的 1 MiB 不变量

第一版补丁没有发明新的配置项,而是沿用已经存在的 maxCharsPerRecord = 1 MiB

  • CSV splitter 与 line-delimited JSON 在 buffer/parse 前拒绝超长记录;
  • 保留最早发生的 splitter error,不让 partial decode 覆盖;
  • 将错误透传为 OverMaxRecordSize,不再折叠成 InternalError

这是一项有意的兼容性收缩。过去包含超过 1 MiB 单行或单记录的客户端,升级后必须切分输入。

第二轮:硬件相关的绕过

六月沿调用链继续检查时发现:

JSON Lines -> simdj.NewReader -> simdjson.ParseNDStream

simdjson.SupportedCPU() 为真时,JSON Lines 绕过 bounded json.PReader。第三方 parser 会在 chunk 结束后继续读取直到换行,普通 reader wrapper 无法同时做到“不丢失前面完整记录”和“下一条记录一定有界”。

最终选择不是继续包裹,而是让 JSON Lines 暂时全部走 bounded PReader。未来如果恢复 SIMD,它必须自己执行同样的 record bound,并通过同一组不依赖 CPU 的回归测试。

同一轮修正的流语义

复核还修正了几个相邻问题:

  • errors.As 透传实现 SelectError 的错误,而不是只识别一个 concrete type;
  • JSON worker 把 parser error 包装成 JSONParsingError
  • terminal error event 之前先 flush 已完成但不足 batch size 的 output queue;
  • 保留输入顺序中的错误优先级,不用更晚的 oversized record 覆盖更早的 parse error。

这些细节决定了客户端看到的是正确的失败,而不是“修复了资源上限,却破坏了流式协议”。

刻意没有塞进本 CVE 的问题

  • CSV AllowQuotedRecordDelimiter 与外层物理换行 splitter 的历史语义缺陷;
  • CRLF 中 \r 是否计入长度;
  • 在没有相同边界的情况下恢复 SIMD 性能。

这些问题有的真实存在,但需要独立的 AWS compatibility evidence 或更复杂的 quote-aware splitter,不适合借安全修复顺手猜答案。

验证与发布

历史记录包含 oversized JSON Lines、错误码保留和不依赖本机 SIMD 能力的行为测试;go test ./internal/s3select/... -count=1git diff --check 均有通过记录。

公开初始修复提交为 c5765dc,六月完整修复为 fd69c89。本次博客整理没有重新执行这些测试。

最终代价

  • JSON Lines 性能可能下降,本事件没有 benchmark 给出量化结果;
  • 1 MiB 单记录上限会拒绝过去可接受的超大输入;
  • quoted CSV multiline 语义仍需独立处理;
  • 任何 CPU-specific fast path 以后都必须与 slow path 共用安全测试。

这次二次修复留下的教训是:安全不变量必须跨硬件路径成立。只在当前 CPU 上跑绿的测试,不能证明另一个执行引擎也受保护。

CVE-2026-40344:Snowball 自动解包认证绕过

Snowball unsigned-trailer 请求可以在完成认证前进入解包器;修复把 SigV4 验证前移到任何 tar 字节之前。

状态: 已发布
首个包含版本: RELEASE.2026-04-17T00-00-00Z
GitHub Advisory: GHSA-9c4q-hq6p-c237

Snowball PutObjectExtractHandler 漏掉了 streaming unsigned-trailer auth case。伪造 signature 的 tar stream 可以在认证完成前进入 untar(),而一次请求又能扇出为多个对象写入。最终修复在任何 tar 字节进入解包器之前,初始化正确 reader、处理 decoded length 并完成 SigV4 验证。

编号为什么变过

修复时正式 CVE 尚未分配,commit subject 使用了临时的 fake CVE-2026-40028。正式编号后来确定为 CVE-2026-40344。历史 commit 没有重写;公开 advisory 与本文一律使用正式编号。

从一次认证遗漏到批量对象写入

入口是 Snowball / PutObjectExtract 自动解包。请求采用 unsigned-trailer streaming,而旧 handler 没有像普通 PUT 一样覆盖该 auth type。

危险不只在于一个请求被错误授权。tar stream 一旦进入 untar(),单个请求可以创建多个攻击者指定的对象。认证错误因此被放大成批量写入问题。

最终不变量:失败时解包器必须看到零字节

修复过程中最关键的一句话是:

如果认证最终失败,untar() 必须看到零字节。

这直接排除了“先解包,认证失败后再回滚”的方案。对象写入会经过多条路径,要证明回滚完整远比证明输入从未越过边界困难。正确收口点只能在数据流进入解包器之前。

实现

最终改动包括:

  • 识别 authTypeStreamingUnsignedTrailer
  • 读取 X-Amz-Decoded-Content-Length
  • 使用 newUnsignedV4ChunkedReader()
  • 在进入 untar() 前执行完整 SigV4 request verification;
  • 保留合法 signed Snowball 与 CRC32 trailer flow。

验证

历史 commit 与会话记录覆盖:

  • forged-signature Snowball unsigned-trailer 被拒绝;
  • non-public bucket 的 anonymous Snowball 被拒绝;
  • 合法签名与 trailing CRC32 可以正常解包;
  • vulnerable parent 与 patched tree 的红绿对照;
  • containerized before/after smoke。

公开修复提交为 b50ab58。本次博客整理没有重新运行容器测试。

兼容性与残余风险

  • 过去依赖实际未验证的 unsigned-trailer Snowball 组合的客户端,升级后会失败;
  • 认证已经前移,但 tar 内容路径、归档大小与对象数量限制仍是独立安全面;
  • Snowball 与普通 unsigned-trailer 现在共享 reader,未来修改必须同时回归两条路径。

这个事件的重点不是多加了一次 if,而是把认证决定移动到真正的放大边界之前。

CVE-2026-41145:Unsigned-Trailer 查询认证绕过

query-string SigV4 credential 进入 unsigned-trailer 流后没有被验签,最终在共享 reader 边界统一关闭。

状态: 已发布
首个包含版本: RELEASE.2026-04-17T00-00-00Z
GitHub Advisory: GHSA-hv4r-mvr4-25vw

query-string SigV4 credential 可以进入 STREAMING-UNSIGNED-PAYLOAD-TRAILER 数据流,但旧代码只在存在 Authorization header 时验证签名。结果是:请求只要提供有效 access key 标识,即使没有正确 signature,也可能完成写入。

最终修复把 presigned rejection 与 SigV4 verification 放进 newUnsignedV4ChunkedReader(),让所有消费该数据流的 caller 共用同一认证边界。

编号说明

修复时正式编号尚未分配,commit subject 临时写为 fake CVE-2026-40027。正式编号后来确定为 CVE-2026-41145。历史 commit 保留原样,公开材料统一使用正式编号。

根因:把认证绑定到传输形式

受影响入口包括 PutObjectPutObjectPart。请求使用 STREAMING-UNSIGNED-PAYLOAD-TRAILER,credential 与 signature 放在 query string,而不是 Authorization header。

旧 handler 用“header 是否存在”决定要不要验签。body reader 却仍然正常读取数据,于是 query auth 被静默降级成近似匿名写入。攻击者只需要知道一个有效 access key 标识,并不需要构造正确 signature。

问题不是 query 参数没有解析,而是认证决定依赖凭据的传输形态,而不是真正消费数据流的信任边界。

为什么不逐 handler 打补丁

方案风险结论
PutObjectPutObjectPart 各补一段 header/query 判断当前入口能闭合,但新 caller 很容易再次遗漏否决
发明 presigned unsigned-trailer 兼容协议协议与测试面显著扩大,又没有既有支持契约否决
newUnsignedV4ChunkedReader() 统一拒绝并验签所有消费者强制经过同一边界接受

匿名 unsigned-trailer 并没有被一刀切禁用。如果 bucket policy 明确允许 anonymous write,它仍然可以按匿名授权路径工作。真正被禁止的是“带 query credential,却没有验证 credential”的混合状态。

实现与验证

修复在 cmd/streaming-v4-unsigned.go 的 reader 入口完成 presigned rejection 与 SigV4 verification,同时移除 PutObject / multipart handler 中依赖 header presence 的 gate。

新增测试覆盖 forged query PUT、multipart、mixed auth 与 anonymous policy。历史记录还包括 vulnerable parent 上写入成功、patched tree 上失败,以及 header-authenticated 与合法 anonymous flow 继续工作的 live server before/after smoke。

公开修复提交为 fa7c579。本次博客整理没有重新执行 live exploit。

兼容性与残余风险

  • presigned/query unsigned-trailer 现在明确不受支持,这是有意的 breaking change;
  • 修复下沉到 reader,显著降低 sibling handler 再次漏检的风险;
  • 其他 streaming auth mode 仍然需要独立审计,不能因为这一条 reader 修复就宣称所有 SigV4 streaming 组合安全。

这次修复的形状比 payload 本身更重要:当多个 handler 共享同一种认证数据流时,认证应该属于 reader,而不是每个 caller 的可选判断。

CVE-2026-42600:ReadMultiple Storage-REST 路径穿越

从完整 preflight 校验到删除整条 API:一个没有生产调用者的内部文件读取接口为什么不值得保留。

状态: 已发布
首个包含版本: RELEASE.2026-06-18T00-00-00Z
GitHub Advisory: GHSA-xh8f-g2qw-gcm7
影响范围: 仅 distributed erasure;需要 cluster-root / internode JWT

/rmpl 的 msgpack body 中包含 BucketPrefixFiles,旧代码直接把它们拼成文件系统路径,没有 containment。最初的修复实现了完整 preflight validation;继续审计调用链后却发现,这条 API 从 2024 年起已经没有 production caller。最终方案因此从“保留并加固”转为删除 route、handler、client、interface 与生成代码。

删除约一千行代码看似比局部校验更大,长期攻击面却更小。

威胁模型

这个漏洞只在 distributed erasure 模式下注册,single-node 部署不受影响。攻击者需要 root secret 派生的 internode JWT、被控节点,或者能够截获未加密的节点间流量。

危险字段位于 msgpack body,不在 URL 或 form 中,因此上层 HTTP path middleware 看不到。xlStorage.ReadMultiple 会直接 join 并读取,允许路径逃出 drive root。

这不是匿名 S3 漏洞,而是从“集群 root / peer”到“节点进程可读文件系统”的边界跨越。

第一版:保留 API,完整校验

最初补丁在 xlStorage.ReadMultiple 中:

  • 拒绝 absolute path、. / .. segment、反斜杠、Windows drive prefix 与 NUL;
  • 校验 drive → volume → prefix → file 的最终 containment;
  • 尽量保留空 Bucket 与 .minio.sys/multipart 历史契约;
  • 在任何 read 或 streaming 发生前返回错误。

这套设计本身可以关闭已知穿越,但审查很快发现了一个 early-return 缺口。

MaxResults 暴露了“边用边校验”的问题

第一版在读取循环里逐项验证 Files。如果请求是 Files=[good, bad]MaxResults=1,函数读取第一项后提前返回,第二项永远不会被校验。

它没有直接读取第二个恶意文件,却破坏了“整个 msgpack request 必须先合法”的修复目标。于是校验被前移成全量 preflight,path length 也在 streaming 前统一检查。

这次转折留下了一条通用规则:存在 early return 或 streaming 的请求,逐项使用前检查不等于整请求安全验证。

最终决定:删除 API

进一步调用链审计确认:

  • 上游在 2024 年 9 月移除了最后一个 production caller;
  • multipart 已改用 ReadParts
  • 当前树没有 in-tree production consumer;
  • 上游正式处置也选择删除 ReadMultiple

最终删除 route、handler、client wrapper、StorageAPI / xlStorage method、metric、datatype 与生成代码,storageRESTVersion 保持原有兼容策略。

方案短期变化长期维护面结论
原地 validationdiff 较小,保留接口永久保留无人使用的高权限文件读取 API放弃
删除 API删除较多接口与生成代码攻击面与维护面最小接受

验证与发布

原地校验阶段运行过 xlStorage、storage-REST client、msgpack encode/decode 与 path edge case focused tests;对抗性 review 补出了 MaxResults 问题。删除阶段检查了 route、client、interface、generated surface 与 caller absence。

公开修复提交为 73ac524,并随 SILO 2026-06-18 发布。本次博客整理没有重新运行删除后的 full suite。

兼容性与结论边界

  • 外部 S3 API 没有变化;
  • 私自调用内部 /rmpl 的第三方实现会失效;
  • mixed-version rolling upgrade 可能出现 protocol mismatch,因此升级时应保持节点版本一致;
  • 删除 endpoint 只证明 ReadMultiple 不再存在,不能外推成所有内部节点 body path 都已经完成 containment 审计。

这个 CVE 的最终修复是正确的,但它也提醒我们:关闭一个 endpoint,与关闭一个缺陷类别,是两种不同结论。