必须流式io.copy读取,禁用io.readall以防oom;需filepath.clean路径并二次校验前缀防zip slip;rc.close()不可遗漏,否则句柄泄漏;大文件应加context超时控制。

用 zip.OpenReader 遍历文件时必须流式 io.Copy
直接读取整个 zip.File 内容到内存(比如用 io.ReadAll(f.Open()))会把所有文件加载进 RAM,几百 MB 的 ZIP 就可能触发 OOM。标准库不帮你做流控,得自己约束。
- 对每个
f *zip.File,调用f.Open()得到io.ReadCloser,别用f.Content()(它只适用于小文件且已加载) - 用
io.Copy(dst, src)转发数据,它内部自动分块、缓冲、错误中断,比手动Read/Write更可靠 - 记得
defer rc.Close(),否则文件句柄泄漏,解压中途 panic 会导致后续文件无法打开
解压路径校验必须用 filepath.Clean + 前缀检查
不校验路径等于给 Zip Slip 漏洞开门——攻击者传一个 ../../../etc/passwd 的文件名,你的 os.Create 就真往系统目录里写。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 对每个
f.Header.Name先调cleanPath := filepath.Clean(f.Header.Name) - 再判断
strings.HasPrefix(filepath.ToSlash(cleanPath), ".") || strings.Contains(cleanPath, ".."),满足任一就跳过 - 构造目标路径用
dstPath := filepath.Join(outputDir, cleanPath),之后再用strings.HasPrefix(filepath.ToSlash(dstPath), filepath.ToSlash(outputDir))二次确认没逃出根目录
os.MkdirAll 创建目录时权限不能硬写 0755
源 ZIP 里的目录可能有不同权限(比如 0700),直接写死 0755 会丢失原始语义,还可能在严格模式下被拒绝创建。
- 从
f.Header提取权限:若f.IsDir(),可用f.Header.Extra或更稳妥地——依赖archive/zip未导出字段的替代方案是忽略,或按需保留;实际中多数 ZIP 不带精确权限,0755是合理 fallback - 但必须确保父目录存在:
os.MkdirAll(filepath.Dir(dstPath), 0755),否则os.Create会因路径不存在失败 - 注意
os.MkdirAll不会覆盖已有目录权限,只建缺失部分
大文件解压时别让 zip.Reader 缓冲失控
zip.OpenReader 默认把整个 ZIP 索引结构加载进内存,但真正危险的是解压单个大文件时,如果目标磁盘满或网络中断,io.Copy 会卡住,goroutine 无法释放。
- 加 context 控制超时:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute),并在io.Copy外层包装io.CopyN或自定义带 ctx 的 copy 函数 - 避免并发解压同一 ZIP 文件——
*zip.ReadCloser不是线程安全的,多个 goroutine 同时调f.Open()可能导致读取错位 - 如果 ZIP 包含上百个小文件,遍历本身很快,瓶颈在磁盘 I/O;可考虑用
sync.Pool复用bytes.Buffer作临时缓冲,但对纯流式场景收益有限
filepath.Clean 必须在 os.MkdirAll 和 os.Create 之前做,且一旦跳过某个文件,它的 rc.Close() 仍要执行——否则下一个文件的 f.Open() 可能失败。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










