tar.newreader 必须接收未被预读的 gzip.reader,否则因 gzip 预读导致 tar header 解析错位;解压需严格按 header.size 读取、路径须 clean+白名单校验、gzip 流 eof 需正确判断。

tar.NewReader 传入 gzip.Reader 前,必须确保没被预读过
直接把 gzip.NewReader(f) 的返回值塞给 tar.NewReader(),解出来的文件名、大小、权限全错——不是 bug,是设计使然。gzip.NewReader 内部会预读至少 10 字节来校验 gzip header,而 tar.NewReader 期望从流最开头解析 512 字节的 tar header,位置已经偏移了。
- 正确做法:用原始
*os.File或可重放的io.Reader(如bytes.Reader)创建gzip.Reader,再把这个gzip.Reader直接传给tar.NewReader;它自己就是合法的io.Reader,无需“取底层” - HTTP 流等一次性 reader 必须先用
io.TeeReader或bytes.Buffer缓存前若干字节做 magic 检查(0x1f 0x8b),再决定是否走 gzip 分支 - 千万别在调用
gzip.NewReader前对源 reader 调用Read()、Peek()或Seek()—— 否则预读 + 预读 = 错位加倍
解压时必须按 Header.Size 精确读取,不能读到 io.EOF
用 io.Copy(outFile, tr) 解单个文件?危险。它会一直读到 tar 流结束,把下一个文件的 header 当作当前文件内容写进去,后续 tr.Next() 直接 panic 或跳过条目。
-
tar.Reader.Next()返回 header 后,必须严格读取header.Size字节,不多不少 - 推荐用
io.CopyN(outFile, tr, header.Size)—— 它自动分块(默认 32KB)、可控、不内存暴涨 - 如果要限速或统计进度,套一层
io.LimitReader(tr, header.Size)再传给io.Copy,别碰Size做预分配切片 - 注意:tar 格式要求每个文件数据块末尾补 0 对齐 512 字节,
tar.Reader的Next()会自动跳过 padding;但如果你手动Read(),就得自己对齐,否则下个Next()错位
路径遍历漏洞不止是过滤 "..",clean + prefix 白名单才可靠
header.Name 是攻击面。传入 "../../etc/passwd"?标准库照单全收,filepath.Join(dest, header.Name) 一拼就出事。Go 不做任何 sanitize,这事你得扛。
- 先用
filepath.Clean(header.Name)归一化,消除./、../、重复斜杠等干扰 - 再检查:
cleanPath != header.Name || strings.HasPrefix(cleanPath, "..") || strings.Contains(cleanPath, "\x00") || cleanPath == "" || cleanPath[0] == '/'→ 全部拒绝 - 最后白名单校验:
filepath.HasPrefix(filepath.ToSlash(cleanPath), "expected/subdir/"),确保解压目标始终落在你指定的根目录内 - Windows 路径要注意:
filepath.FromSlash()统一转成本地格式再校验,避免"..\foo"绕过
gzip.Reader 读完不等于 io.EOF,循环里必须显式判断
常见卡死现象:解压循环一直跑,CPU 占满,日志没报错也没结束。因为 gzip.Reader.Read() 在流末尾可能返回 n > 0, err == nil,再读一次才返回 io.EOF,但很多人只判 err != nil 就 break。
- 最安全解法:用
io.Copy(dst, gr),它内部已完整处理 EOF 和缓冲边界 - 若需逐块读(比如加哈希、限速),每次
Read(p []byte)后必须同时检查:if n == 0 && err == io.EOF或err != nil && err != io.EOF -
gzip.Reader.Close()只释放资源,不表示数据读完了;别拿它当流结束信号 - 错误信息如
gzip: invalid header通常意味着输入不是 gzip 格式,或开头被截断(例如 HTTP chunked 编码未解码)
真正麻烦的从来不是怎么写通,而是 tar.gz 两层嵌套带来的隐式状态:gzip 的预读、tar 的块对齐、路径的归一化时机、Size 的精度依赖……少一个环节,解出来就是乱码或越界写入。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











