tar.gz 压缩解压必须严格遵循 gzip.NewReader→tar.NewReader(读)和 tar.NewWriter→gzip.NewWriter→os.Create(写)的嵌套顺序,且 Typeflag 必须显式设置、Size 必须用 io.CopyN 严格读取、路径需 clean 并校验防止遍历。
tar.gz 压缩必须先 tar 再 gzip,顺序反了就解不开
Go 里没有“一键 tar.gz”函数,
和
是两个独立职责的包:tar 负责打包(合并多个文件+路径结构),gzip 负责压缩(单流字节压缩)。你不能用
包住一个
的输出流以外的东西,也不能把未压缩的
直接丢给
—— 那读出来全是乱码。
错误写法:
(
是 .tar.gz 文件)→ 报错
或解出空名、零大小文件
正确嵌套:
→ 得到
→ 传给
写入时也得严格嵌套:
→
→
,且
必须在
关闭前
漏掉
或
,生成的 .tar.gz 用
检查会报
写 tar header 不设
就算内容对也解不开
的
字段不是可选的,Go 的
对它做严格校验。默认值是
(即
),而 GNU tar 会容忍并尝试推测,但 Go 不会——它直接跳过该条目或报错
。
普通文件必须显式写:
目录必须写:
,且
末尾带
(如
),否则会被当成普通文件处理
别依赖
自动推断类型;
控制权限,
控制条目语义
示例:
解压 tar.gz 时不按
读就会串位
返回一个 header 后,后续数据就是这个文件的内容,长度正好是
。如果你用
一直读到
,就会把下一个文件的 header 也吞进去,导致后续
解析失败、文件名错乱、甚至 panic。
安全做法:用
,它保证只读指定字节数
千万别用
或循环
直到
—— 这破坏 tar 流结构
目录条目(
)的
是 0,此时跳过
,只调
如果要限速或打日志,套
再传给
,而不是自己管理缓冲
路径遍历漏洞不校验,
就真写进系统了
tar 和 zip 一样,
完全由打包方控制。Go 标准库不做任何路径 sanitize,
一旦遇到
,就可能覆盖任意位置。这不是理论风险,而是真实 CVE 场景。
立即学习
“
go语言免费学习笔记(深入)
”;
必须先
再检查:
→ 直接跳过该条目
创建文件前,用
确保父目录存在;目录条目要单独判断
并
Windows 路径分隔符也要处理:
统一后校验,避免
绕过
最常被忽略的一点是:所有
都不能靠
在同一函数里混用读写句柄——比如写完
立刻用
读它,此时
还没执行,文件内容可能还在内核缓冲里。分阶段、显式
,才是流式归档稳住的关键。
archive/tarcompress/gzipgzip.NewWritertar.Writer*os.Filetar.NewReadertar.NewReader(f)farchive/tar: invalid tar headergzip.NewReader(f)io.Readertar.NewReader()os.Creategzip.NewWritertar.NewWritertar.Writergzip.WriterClose()tw.Close()gw.Close()gunzip -tunexpected end of fileTypeflagtar.HeaderTypeflagtar.Reader0\x00archive/tar: invalid headerhdr.Typeflag = tar.TypeReghdr.Typeflag = tar.TypeDirName/"logs/"file.Stat().Mode()ModeTypeflaghdr := &tar.Header{Name: "config.json", Size: 128, Mode: 0644, Typeflag: tar.TypeReg}Header.Sizetar.Reader.Next()header.Sizeio.Copy(dest, tr)io.EOFNext()io.CopyN(dest, tr, header.Size)io.ReadAll(tr)tr.Read(buf)err == io.EOFTypeDirSizeCopyNos.MkdirAll(...)io.LimitReader(tr, header.Size)io.Copy../../etc/passwdHeader.Namefilepath.Join(dstRoot, header.Name)"../"cleanPath := filepath.Clean(header.Name)cleanPath != header.Name || strings.HasPrefix(cleanPath, "..") || strings.HasPrefix(cleanPath, "/")os.MkdirAll(filepath.Dir(dstPath), 0755)header.Typeflag == tar.TypeDiros.MkdirAll(dstPath, header.Mode)filepath.ToSlash(header.Name)"..\foo"Close()defer.tar.gzos.Opendefer gw.Close()Close()