Go解压ZIP必须校验路径防Zip Slip、及时关闭单条目句柄防资源泄露、流式io.Copy防OOM、UTF-8中文名需手动解码;zip.OpenReader前须检查文件完整性,避免unexpected EOF。

Go 解压 ZIP 文件不是调个 zip.OpenReader 就完事——不校验路径会写穿系统目录,不关句柄会卡死,不流式处理会 OOM,中文名不处理直接变乱码。
解压前必须检查 io.ErrUnexpectedEOF 和文件完整性
直接 zip.OpenReader("broken.zip") 可能 panic 或静默失败,常见报错是 zip: not a valid zip file 或更隐蔽的 unexpected EOF。这不是你代码错了,而是 ZIP 数据本身损坏或截断。
- 先用
os.Stat确认文件存在且Size() > 0 - 打开后立即注册
defer r.Close(),但别忽略r.Close()返回的 error —— 若是io.ErrUnexpectedEOF,说明解压中途就断了 - 从 HTTP 或流式输入读取时,别直接传
resp.Body给zip.NewReader;必须先io.ReadAll(resp.Body)拿到完整字节,再用bytes.NewReader(data)构造 reader
路径校验必须做两步:clean + hasPrefix
archive/zip 对 f.Name 完全放行,../../../etc/passwd 这种名字它照拼不误。不做校验等于给攻击者开后门。
- 第一步:用
filepath.Clean(f.Name)归一化,把./a/../b变成b,把../x变成../x - 第二步:检查归一化后的路径是否仍含
".."、是否以"/"开头(Linux/macOS),Windows 下还要统一转小写、把"\"替换为"/" - 第三步:构造目标路径
dstPath := filepath.Join(absDest, cleanName),再用strings.HasPrefix(filepath.ToSlash(dstPath), filepath.ToSlash(absDest))二次确认没逃出根目录
循环里 defer rc.Close() 是陷阱
在 for _, f := range r.File 循环里写 defer rc.Close(),会导致所有 io.ReadCloser 堆积到函数退出才释放。几百个文件就触发 too many open files。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:在每次
rc, _ := f.Open()后立刻写defer rc.Close()—— 它只作用于当前条目 - 整个 ZIP 读取器的
r.Close()必须在遍历前就defer,不能漏 - 别用
os.Open+zip.NewReader替代zip.OpenReader;后者自动定位 EOCD,更健壮
中文文件名不能靠 header.SetUTF8(true) 一招鲜
Windows 资源管理器打的 ZIP 默认用 GBK 编码文件名,而 Go 的 archive/zip 总按 UTF-8 解析 f.Name,结果就是一堆 ???.txt。
- 优先让上游改用 UTF-8 打包:7-Zip 勾选 “UTF-8”,macOS 默认支持,Linux 用
zip -U - 若必须兼容旧 ZIP,用
golang.org/x/text/encoding/simplifiedchinese手动 decode:decoded, _ := gbk.NewDecoder().String(f.Name) -
header.SetUTF8(true)只在 Go 1.22+ 有效,且依赖 ZIP 包本身设置了 flags 位(bit 11),不可靠
最常被跳过的三件事:路径校验、单条目 rc.Close()、压缩时没设 header.Method = zip.Deflate —— 解压环节漏掉前两项,基本等于裸奔。


















