go解压zip需严格校验路径:先clean再检查是否含“..”或以“/”开头,防止目录穿越;必须defer r.close()防文件描述符泄露;用io.copy流式解压防内存暴涨;中文名需检查flags位并降级处理。

解压 ZIP 时路径没校验,../../../etc/passwd 就能写到系统目录
Go 的 archive/zip 不做任何路径安全检查,直接拼接 filepath.Join(dest, f.Name) 是高危操作。攻击者只要在 ZIP 中放一个名字为 ../../config.yaml 的文件,解压就可能覆盖你服务的配置甚至系统关键文件。
必须对每个 f.Name 做两步处理:
- 先用
filepath.Clean(f.Name)归一化路径(把./a/../b变成b,..x转成../x) - 再检查归一化后的路径是否仍含
"..",或是否以"/"开头(Windows 下还要统一转小写、替换"\"为"/") - 最后确认
filepath.ToSlash(dstPath)确实是以filepath.ToSlash(absDest)开头,否则跳过该条目
zip.OpenReader 后忘记 defer r.Close(),后续解压会卡住或报错
ZIP 文件内部是随机访问结构,zip.OpenReader 会打开底层文件并维护索引映射。不调用 Close() 不仅泄露文件描述符,还可能导致后续读取失败(比如返回 invalid zip file 或 EOF 异常)。
尤其注意循环中不要写 defer rc.Close() —— 这会导致所有文件句柄堆积到函数退出才释放,几百个文件就容易触发 “too many open files” 错误。
- 正确做法:在
for _, f := range r.File循环内,rc, _ := f.Open()后立即用defer rc.Close(),但只针对当前条目 - 整个 ZIP 读取器的
r.Close()必须放在defer里,且在遍历前就注册 - 别用
os.Open+zip.NewReader替代zip.OpenReader,后者自动定位 EOCD,更健壮
大文件解压时内存暴涨,io.Copy 必须流式走,不能 io.ReadAll
ZIP 解压本身不缓存全文,但如果你对每个条目调用 io.ReadAll(rc) 再写入磁盘,等于把整个 ZIP 所有内容都加载进内存 —— 一个 500MB 的 ZIP 可能吃掉 1.2GB 内存(Go 的 slice 扩容策略+冗余缓冲)。
io.Copy 是唯一推荐方式,它默认使用 32KB 缓冲区,全程零拷贝流式传输。
- 确保目标文件用
os.O_CREATE | os.O_WRONLY | os.O_TRUNC打开,避免追加写乱数据 - 创建父目录用
os.MkdirAll(filepath.Dir(dstPath), 0755),别依赖 ZIP 中的f.Mode()(不可信) - 如果要限速或监控进度,可在
io.Copy前包一层自定义io.Reader,而不是换读取方式
中文文件名乱码或解压失败,zip.FileHeader.SetUTF8(true) 不是万能的
Go 1.22+ 支持 UTF-8 文件名,但前提是 ZIP 包本身在生成时已设置通用位标志(general purpose bit 11)。很多第三方工具(如 Windows 自带压缩、旧版 7-Zip)默认不设,导致 Go 解压时把 UTF-8 字节当 Latin-1 解码,显示为 .txt。
实际能做的只有两件事:
- 解压端:用
f.Header.Flags & 0x800 != 0检查 ZIP 是否声明了 UTF-8 编码,仅在此条件下信任f.Name - 生成端:压缩时显式调用
header.SetUTF8(true)(需 Go ≥1.22),并确保用filepath.ToSlash()统一分隔符 - 兼容方案:若不确定来源,可对
f.Name做strings.ToValidUTF8()降级处理,或 fallback 到拼音/UUID 重命名
空目录、Windows 路径分隔符、权限丢失……这些都不是边缘情况,而是 ZIP 规范和 Go 实现之间真实存在的缝隙。填缝靠的是每一步的显式判断,不是“默认应该可以”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











