首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >我把 Gohttpserver 的 Zip Slip 拆到了源码级,问题不在解压那几行

我把 Gohttpserver 的 Zip Slip 拆到了源码级,问题不在解压那几行

原创
作者头像
Lethehong
发布于 2026-11-12 12:23:48
发布于 2026-11-12 12:23:48
140
举报
文章被收录于专栏:网络安全网络安全

先说结论,这是路径拼接的老毛病

Zip Slip 不是新东西。2018 年 Snyk 披露的时候,一批 Java 生态的项目集体中招,核心就一句话,压缩包里的文件名是攻击者可控的,程序拿它去拼路径,没做规范化就直接写文件。

Gohttpserver 这次被公开的问题,性质一样。它提供了上传压缩包并解压的功能,解压时把压缩包内的条目名当相对路径用,如果条目名里带 ../,拼出来的目标路径就会跑到解压根目录之外。

我看到这条的时候,第一反应不是「又一个」,而是「这个位置太典型了」。文件上传加解压,几乎是 Web 应用里最容易出路径穿越的组合,因为它同时具备两个条件,外部可控的输入,和直接落到文件系统的动作。

为什么现在值得看,不是因为漏洞新,是因为 Gohttpserver 这类工具在内部环境里铺得很开。它常被拿来当临时文件服务器、制品分发、内网共享盘用,部署的人往往觉得「这是内网,没事」。这个判断本身就有问题,后面我会单独说。

这里我先把自己的立场摆出来。这条消息是 2026 年 11 月 12 日由先知社区首次公开的,我看到的时候已经是当天下午。我没有抢首发,也不打算抢,因为抢首发的人往往只写「有洞,快修」,不写这个洞长在代码的哪一层。

缺陷不在解压,在拼路径之前少了一步

我把这类实现拆开看,链路基本都是四步。用户上传一个 zip,服务端收到后交给解压逻辑,解压逻辑遍历每个条目,取出条目里的文件名,然后用解压根目录加上这个文件名拼出目标路径,最后创建文件写入。

问题出在第三步和第四步之间。拼路径这个动作本身没错,错的是拼完之后没有对结果做规范化再比对。正确的做法是,把拼出来的绝对路径做一次 Clean,然后判断它是否仍然以解压根目录为前缀,不是就拒绝。

Go 标准库里的 filepath.Clean 能消掉 ../,但它只做字符串层面的规整,不做边界判断。很多人以为调了 Clean 就安全了,其实 Clean 完的路径可能已经跑到根目录外面,你只是把它写得更整齐了。

图 1 本文示意图:压缩包条目名一路流到文件系统,卡点本该在拼路径之后

还有一个更隐蔽的点,压缩包条目名在不同实现里的编码和分隔符处理不一致。Windows 风格的 ..\ 在某些解压库里也会被当成路径分隔符,如果校验只拦了 ../ 没拦 ..\,等于只堵了一半。

所以真正的失效点不是「没校验」,是「校验的位置和校验的对象都不对」。校验应该发生在规范化之后的最终路径上,而不是发生在原始条目名的字符串匹配上。

我补一句我自己的判断。这类代码我见过太多,写的人不是不知道要校验,是校验写在了错误的时机。先匹配字符串再拼路径,和先拼路径再校验边界,是两种完全不同的安全模型,前者永远补不干净。

什么调用顺序才能走到这一步

触发这个缺陷需要几个前提同时成立。第一,目标实例开启了上传功能,很多部署默认是开的,因为它本来就是给人传文件的。第二,上传接口允许 zip 类型,或者没有对扩展名做严格限制。第三,解压动作是自动触发的,或者有一个能被普通用户调到的解压入口。

这三个条件里,第三个最关键。如果解压需要管理员手动点,攻击面就小很多。如果上传完自动解压,那就是一个普通上传请求就能打到文件系统。公开信息里没有说 Gohttpserver 的解压是自动还是手动,这一点我没查到,只能按同类工具的常见做法推断,多数是上传后由用户触发解压。

一旦能走到解压,攻击者能做的事就很直接了。往 Web 根目录写一个脚本文件,或者往配置目录覆盖一个配置文件,或者往 cron 目录写定时任务。写到哪,取决于运行 Gohttpserver 的进程有什么权限。

这里有个细节我想单独说。Go 程序通常以单个二进制部署,很多人图省事直接用 root 跑,或者用有写权限的服务账号跑。进程权限越大,这个路径穿越的落点就越致命。

还有一个反直觉的地方。很多人以为只要解压根目录设成一个空目录就安全了,其实不是。如果进程对这个空目录的父目录有写权限,../ 一样能跳出去。边界不是目录本身,是进程权限能触达的范围。

图 2 本文示意图:接口层只管收文件,解压层只管拆包,边界判断没人做

正确的边界校验写法(示意代码,演示拼接后规范化再比对前缀,防止路径跑出解压根)

黑盒怎么确认,白盒怎么定位

黑盒复现的思路很朴素。构造一个 zip,里面放一个条目,文件名写成 ../../../../tmp/zipslip_probe.txt,内容随便写一行。上传,触发解压,然后去看 /tmp 下有没有这个文件。有,就说明路径穿越成立。

这一步我没法给你截图,因为我手上没有现成的 Gohttpserver 实例,也没有在真实环境里跑过。我能做的是把构造方法和判断逻辑写清楚,你自己在测试环境里验。

图 3 本文示意图:公网加自动解压是最该今天动手的那一格

白盒定位更直接。去源码里搜解压相关的调用,重点看三处,zip.OpenReader 之后的条目遍历,filepath.Join 的调用点,以及 os.Create 或 os.OpenFile 的目标路径来源。如果 filepath.Join 的参数里直接出现了条目名,而后面没有跟一次前缀校验,那就是它。

构造探测用压缩包(示意代码,生成带 ../ 条目名的 zip,用于在测试环境验证穿越是否成立)

同类实现里做得对的写法是,先算出目标绝对路径,再算出解压根的绝对路径,然后用 strings.HasPrefix 或者 filepath.Rel 判断目标是否在根之内。这一步不能省,也不能用字符串包含来判断,因为 /data/www 和 /data/www-evil 用 Contains 判断会误放行。

我写了一段自检脚本,用来批量核对你的资产里有没有 Gohttpserver 在跑,以及版本落在哪个区间。版本区间公开信息里给得不完整,这一点后面会讲。

核对在跑的实例(示意代码,筛出本机或网段里疑似 Gohttpserver 的进程与端口)

我怎么验的,以及哪里验不动

这一节我得说清楚,不然就变成无脑吹了。我手上没有真实实例,所以这篇里没有任何一张运行截图,也没有一条我亲手跑出来的报错。所有验证动作都是设计出来的,不是执行出来的。

我能验的部分是逻辑层。我把同类的解压实现对照着看了几遍,确认了四步链路里哪一步会丢边界判断,也确认了 filepath.Clean 单独用不足以拦住 ../。这一段是读代码和读标准库文档得出的,不是跑出来的。

我验不动的部分有三块。真实实例上的触发成功率,我没测。不同 Go 版本下 filepath.Join 对 ..\ 的处理差异,我没逐个版本比对。上传接口对 Content-Type 的实际校验强度,我没有抓包确认。

所以你在自己环境里做验证的时候,建议按这个顺序来。先确认上传入口是否可达,再确认解压是否自动触发,最后才构造探测包。顺序反了容易在第一步就被 WAF 拦掉,白白浪费时间。

探测包我只建议在测试环境里用,别往生产上打。路径穿越这类探测一旦写进去一个文件,清理起来比构造它麻烦得多。

我再补一条我自己踩过的教训。以前做类似验证的时候,我图快直接在共享测试机上打,结果写进去的探针文件没人记得清,最后靠全盘 find 才捞干净。从那以后我都是先建快照再动手。

今天该先关掉哪个开关

优先级第一的事,不是升级,是确认你的 Gohttpserver 有没有把上传和解压暴露出去。如果它只在内网、只给可信的人用、并且上传后不自动解压,那风险等级要往下调一档。反过来,如果它对公网开放,或者对一大群不可信用户开放,今天就该处理。

立刻能做的缓解有三条。第一,在反向代理层把上传接口的请求体大小和 Content-Type 卡住,不允许 zip。第二,如果业务必须传 zip,把解压动作改成人工确认,或者放到一个隔离目录里,用低权限账号跑。第三,给 Gohttpserver 的进程换一个没有写权限目录的账号。

这周该做的,是去官方仓库看有没有修复提交。公开信息里没有给出修复版本号,所以我不敢说「升到某个版本就好」。你能做的是盯住上游的 commit,看解压逻辑里有没有加上规范化后的前缀校验。

如果一时升不了,过渡办法是把解压目录挂成一个独立的、没有执行权限的挂载点,并且用 noexec 挂载。这样即使写进去了,也执行不了。这不是根治,但能把最坏情况压住。

还有一条容易被忽略的,把 Gohttpserver 的访问日志打开,重点看上传接口的请求体里有没有出现 ../ 或 ..\ 的编码形式。这一步不能替代修复,但能给你一个是否已经被扫过的信号。

我把这三条按动手成本排个序。改反向代理规则最快,十分钟能生效。换进程账号次之,要停一次服务。改解压流程最慢,要动业务代码。先做快的,慢的排期做。

这次没查到什么,我不敢替你补

公开信息里缺三块。第一,确切的受影响版本区间没有给全,我只知道这个问题在当前公开报道里被提起,但哪个版本引入、哪个版本修复,我没查到。第二,解压是自动触发还是手动触发,没有明确说法。第三,有没有在野利用,目前没有任何信息。

这三块缺失直接影响风险判断。如果解压是自动的,利用门槛就低一档。如果有在野利用,那就不是「该修」而是「该应急」。现在我只能按最保守的假设来写,也就是按自动解压、按可能被扫来计算。

还有一个推断我要标清楚。我说「问题在路径规范化之后的边界校验缺失」,这是基于同类 Zip Slip 实现的通用规律推出来的,不是我从 Gohttpserver 源码里逐行读出来的。公开描述里提到的是压缩包内文件名路径穿越,这个描述和我的推断吻合,但没有到字段级。

所以这篇的定位是,给你一套可复现的验证路径和一套可落地的缓解动作,而不是一份已经坐实的漏洞分析报告。等上游修复提交出来,或者有更细的技术披露,结论可能要修订。

我不打算在这里编一个版本号出来让你安心。查不到就是查不到,这条我认。

按通用规律推断,同类 Go 解压实现里,出问题的比例不低,因为 Go 生态里很多人习惯直接用 filepath.Join 拼路径,觉得标准库会兜底。这个推断我没有统计数据支撑,只能算经验判断。

这类漏洞的根子,是把「文件名」当成了「可信路径」

Zip Slip 反复出现,不是因为它难,是因为它反直觉。写代码的人看到的是一个文件名,脑子里想的是「这就是个名字」,但文件系统看到的是一个路径,路径里的 .. 是有语义的。

这个错位在所有涉及「外部输入拼路径」的地方都会重演。压缩包解压、文件上传重命名、模板渲染的 include、日志文件的按天切分,都是同一个模式。区别只是触发条件和落点。

我的判断是,修一个 Gohttpserver 不够,得把「拼完路径必须做规范化再校验前缀」变成团队里的固定动作。这件事没法靠扫描器兜底,因为扫描器不知道你的业务路径长什么样。

以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~

谢谢你看我的文章,我们,下次再见。

写在最后

Zip Slip 这类问题,修一个点容易,难的是把「外部输入拼路径必须先规范化再校验边界」变成肌肉记忆。工具会换,语言会换,这条判断不会过期。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

目录
  • 先说结论,这是路径拼接的老毛病
  • 缺陷不在解压,在拼路径之前少了一步
  • 什么调用顺序才能走到这一步
  • 黑盒怎么确认,白盒怎么定位
  • 我怎么验的,以及哪里验不动
  • 今天该先关掉哪个开关
  • 这次没查到什么,我不敢替你补
  • 这类漏洞的根子,是把「文件名」当成了「可信路径」
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档