跳到主要内容
就为了缩一张图,你把整个 libvips 都引了进来

就为了缩一张图,你把整个 libvips 都引了进来

原石
原石

· 阅读约 5 分钟

先贴一行命令。

vips --list-classes | grep -i mat

能看见 ForeignLoadMat 这类东西,你这台机器就编进了 matload——libmatio 那个读 MATLAB v5 文件的加载器。它离一个缩略图服务的距离,比大多数人以为的近得多。

攻击路径不长。上传,ActiveStorage 把 blob 存下来,渲染变体的时候调一次 vips,剩下交给 libvips 自己。它挑哪个加载器,看的是文件头部的 magic bytes,不是你给的扩展名。把一个 MATLAB v5 文件改名成 .jpg 丢上去,头部对得上,它照样往 matload 里走。

Rails 服务器上不需要 MATLAB,不需要任何相关依赖,不需要额外进程。libvips 自己带着这个加载器。链条就这些。

我不太在乎补丁是几个小时内发出的。八小时里冒出真实利用尝试,这数字挺吓人,但它不是重点。重点是:文件上传进来之后,第一个执行验证的代码,就是那个有漏洞的解析器。

你尽可以写 FileUploadValidator,写 content_type 白名单,比对 magic bytes,挂 ClamAV。都行。问题是这些东西要么排在解析之后,要么跑在完全另一条路上。真正第一个碰到这些字节的,是 vips 挑加载器那段逻辑。你把一个"什么文件都能读"的库,放在了处理别人选定文件的位置上。这跟拿字符串拼 SQL 是同一类错误,只是更隐蔽——拼 SQL 是你自己写的,加载器不是你写的。

设计者显然想过这件事。早就有 block_untrusted:

Vips.block_untrusted(true)

或者:

VIPS_BLOCK_UNTRUSTED=1

打开以后,一批被标记为不受信任的加载器干脆不注册,.mat 就在名单里。Rails 这次的修复,本质就是让 ActiveStorage 启动的时候把这一层打开。

然后有意思的地方来了。这个开关,2022 年 5 月 28 日那版 8.13 的发布说明里就写过了,官网的开发者检查清单里也提过。四年,默认是关的。

为什么默认关。因为打开会拦掉一批加载器,而总有人要处理 svg、要处理 pdf、要处理某个冷门到只有他在用的格式。默认拦住,等于默认拒绝一部分用户;默认放行,代价是一个 9.5。

这个选择题设计者留给应用自己答。应用懒得答,于是继承了一个自己根本不需要的能力。

我引 libvips,是为了把用户上传的图缩成 300 像素。我调的 API 就这德性:

image.variant(resize_to_limit: [300, 300])

一次 resize。引进来的是整个 libvips——jpg、png、webp、gif、tiff、pdf、svg、heif、mat、fits、openexr 全能读。它的加载器列表就是它的攻击面列表。我用了它 5% 的能力,继承了 100% 的攻击面。

这句就是我想说的全部。

说句题外话。文件是所有外部输入里最不被当外部输入的一类。HTTP 参数你知道要防,SQL 你知道要防,用户昵称你知道要转义,可文件一进来,绝大多数人的第一反应是"这是用户的数据,我得把它读出来",然后转手交给一个自己没读过的库。因为你下意识觉得文件的"格式"是个客观事实,不由你控制。可加载器的选择权从来不在你手里。

扯回来。

出了这种事,第一反应基本都是加一层扫描。上传网关、隔离沙箱、第三方内容检测、把解析丢进单独容器跑。这些方案我不看好。扫描器对付已知病毒有用,对付一个几十字节就能构造出来的 v5 文件头没用。载荷还能伪装成正常的上传内容——Cloudflare 的托管规则集拦不住,它看到的是一个合法 multipart 请求,文件字节也是合法的。有评论说得很直白,就算前置检查都挡住了,直传 S3 再走变体路由那条路还在。

多加了三个服务、一层网关、一张隔离网,那个加载器还开着。

正确的动作只有一个:不需要的加载器,一个都别留。libvips 给了两种粒度,要么全局关掉不受信任的那一批,要么按需把允许的加载操作限定成你真要的那几种。Rails 这次用的是前者。

顺便,Rails 官方给这个 CVE 的文档,是以 agent skill 的形式发的。我不评价。

还有件事值得单独说。这次的技术细节是提前发布的,因为公开的 PoC 让禁运没了意义。有人抱怨补丁太容易逆向——Rails 修得快,但谁都能从那段 diff 里读出怎么打。这话对,但没用。补丁本来就要发给所有人,包括攻击者。想靠"晚几天公布"来拖住人,跟想靠模糊化来获得安全,是同一种安慰剂。

真正能延后攻击的只有两件事:把不该开的能力关掉,把没修的机器下线。有个评论比我说的准——风险不是"晚打补丁",是"晚打补丁并且让没修的部署继续挂在线上"。

还有人说,把模型指向自己的站点、问一句易不易受这类攻击,模型三分钟就用它自带的文件上传库撸了个能跑的利用出来。我信。构造攻击的成本在往下掉,掉到接近零;防守的成本也在往下掉,掉到一行配置。差的就是你愿不愿意去看这条链。

之前也有人说,很多上传相关的 CVE,靠基本的文件权限和 Web 代理规则就能缓。放在这次这个洞上,不成立。权限是对的、路由是对的、请求是合法的,那个文件还是会被交给 matload。

所以规则就一条:凡是引入一个解析库,先看它默认能读多少种格式,再看你要用几种,把剩下的全关掉。关不掉的,该考虑的不是加一层扫描,是换一个只做这件事的库。

上传是你进程的入口。第一个碰它的代码是你自己选的那个解析器,不是攻击者。这个选择是你的。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →