登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go archive/tar读取归档时限制展开路径的安全方案

来源:17golang原创

时间:2026-09-20 11:46:25 296浏览 收藏

处理用户上传的 tar 包时,真正危险的不是 tar.Reader.Next 本身,而是把 Header.Name 直接拼到目标目录后写盘。名称中的 ../、绝对路径和符号链接都可能让文件离开解压根目录。稳妥的做法是先把条目名当作不可信输入,经过路径边界检查后才创建目录或文件。

官方地址:https://pkg.go.dev/archive/tar

要点速览
  • archive/tar 负责顺序读取,不替业务决定最终写入位置。
  • 先用归档路径规则和 filepath.Rel 验证边界,再调用 os.OpenFile
  • 目录、普通文件、符号链接和大小上限分别处理,不能只做一次字符串替换。

先定义哪些 tar 条目可以落盘

Go 的 tar.Reader 通过 Next 逐项返回 *tar.Header,随后把 Reader 当作当前条目的内容流读取。官方文档还提供了 ErrInsecurePath:在相应的 GODEBUG 设置下,非本地条目名会被报告出来,但它只检查文件名,不检查链接目标。因此业务代码仍要建立自己的写盘策略。

这个示例只接受目录和普通文件,拒绝符号链接、硬链接、设备节点与 FIFO。对上传包来说,这种白名单比“支持所有 tar 特性”更容易审计;如果业务确实需要链接,应先把链接目标解析为根目录内的普通路径,再单独实现。

Go archive/tar 条目名经过路径边界检查后进入可信解压根目录的结构说明图
图1:说明图,展示 tar Header.Name、路径检查和可信根目录之间的边界关系。

用规范化路径和相对路径确认不会越界

不要只用 strings.Contains(name, "..")。它会误伤合法目录名,也漏掉组合路径。先按 tar 使用的斜杠规则清理,再把结果转换成系统路径,最后用绝对路径之间的 filepath.Rel 判断是否回到了根目录之外:

package main

import (
    "fmt"
    "os"
    "path"
    "path/filepath"
    "strings"
)

func safeTarget(root, archiveName string) (string, error) {
    // tar 条目名使用斜杠,先按归档规则判断绝对路径和上级跳转。
    if archiveName == "" || path.IsAbs(archiveName) {
        return "", fmt.Errorf("拒绝绝对或空路径 %q", archiveName)
    }
    clean := path.Clean(archiveName)
    if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
        return "", fmt.Errorf("拒绝越界路径 %q", archiveName)
    }

    rootAbs, err := filepath.Abs(root)
    if err != nil {
        return "", fmt.Errorf("解析根目录: %w", err)
    }
    target := filepath.Join(rootAbs, filepath.FromSlash(clean))
    targetAbs, err := filepath.Abs(target)
    if err != nil {
        return "", fmt.Errorf("解析目标路径: %w", err)
    }
    rel, err := filepath.Rel(rootAbs, targetAbs)
    // Rel 结果以 .. 开头,说明目标已经离开可信根目录。
    if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
        return "", fmt.Errorf("目标路径越出根目录 %q", archiveName)
    }
    // IsLocal 再挡住当前平台上的绝对路径和本地特殊片段。
    if !filepath.IsLocal(filepath.FromSlash(clean)) {
        return "", fmt.Errorf("非本地归档路径 %q", archiveName)
    }
    return targetAbs, nil
}

这里的检查顺序很重要:path.Clean 只负责归档名的词法规范化,不能替代根目录边界判断;filepath.Rel 得到的相对结果也必须按路径分隔符判断,不能用简单的字符串前缀误判 ../backup..data

读取时同时限制类型、单项大小和输出方式

通过安全目标路径后,还要先看 Header.TypeflagHeader.Size,再创建文件。下面的函数保留目录结构,普通文件使用 O_EXCL 防止同一个包静默覆盖已经生成的文件,并把单项大小限制为 64 MiB:

func extractTar(root string, r io.Reader) error {
    tr := tar.NewReader(r)
    const maxEntrySize = 64  maxEntrySize {
                return fmt.Errorf("文件 %q 超过大小上限", hdr.Name)
            }
            if err := os.MkdirAll(filepath.Dir(target), 0750); err != nil {
                return err
            }
            f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
            if err != nil {
                return fmt.Errorf("创建文件 %q: %w", hdr.Name, err)
            }
            _, copyErr := io.CopyN(f, tr, hdr.Size)
            closeErr := f.Close()
            if copyErr != nil {
                return fmt.Errorf("写入文件 %q: %w", hdr.Name, copyErr)
            }
            if closeErr != nil {
                return fmt.Errorf("关闭文件 %q: %w", hdr.Name, closeErr)
            }
        default:
            return fmt.Errorf("拒绝特殊条目 %q,类型 %d", hdr.Name, hdr.Typeflag)
        }
    }
}

完整程序还需要导入 archive/tarfmtiopath/filepath。示例中的 io.CopyN 会严格按头部声明的长度复制当前条目;下一次 Next 会处理 tar Reader 余下的填充。生产环境可以把总条目数、总展开字节数和单层目录深度也设为上限,防止大量小文件拖垮 inode。

Go tar 解压按目录、普通文件、大小上限和特殊类型分流的策略结构图
图2:结构图,展示安全目标计算后对目录、普通文件和特殊条目的处理边界。

把 ErrInsecurePath 和业务检查放进上线清单

检查项处理方式不能忽略的原因
绝对路径或 ../拒绝并记录原始条目名避免目录穿越
符号链接、硬链接、设备节点默认拒绝Header.Name 安全不代表 Linkname 安全
单项与总展开大小在创建文件前检查防止磁盘和内存资源被耗尽
ErrInsecurePath按业务策略拒绝,不直接忽略库提示不是完整的链接安全策略

如果解压根目录是临时目录,创建后应限制权限,并在任务成功或失败时统一清理。不要因为 Next 能够读出条目,就认为它适合直接写盘;读取格式和落盘授权是两个边界。

常见问题

只调用 filepath.Clean 能防止 tar 目录穿越吗?

不能。清理路径后仍需用 Rel 或等价的边界判断确认目标位于根目录内,并单独处理链接条目。

为什么不直接跟随 tar 中的符号链接?

因为链接目标可能指向根目录外,甚至指向设备或敏感文件。除非有明确需求并完成目标解析与边界检查,否则拒绝最容易保持安全。

设置 tarinsecurepath=0 后还需要自己校验吗?

需要。官方文档说明该机制只验证文件名,而且依赖运行时设置;应用自己的类型、大小、总量和根目录策略不能省略。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>