当前位置:首页 > 文章列表 > Golang > Go教程 > Go archive/tar读取归档时限制展开路径的安全方案

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

来源:17golang原创 2026-09-20 11:46:25 0浏览 收藏

处理用户上传的 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删除
Redis Hash与JSON字符串存储对象的选择边界Redis Hash与JSON字符串存储对象的选择边界
上一篇
Redis Hash与JSON字符串存储对象的选择边界
商汤Seko短视频创作者使用指南:从准备素材到导出结果
下一篇
商汤Seko短视频创作者使用指南:从准备素材到导出结果
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    133次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    200次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    146次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    124次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    110次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码