Go archive/tar读取归档时限制展开路径的安全方案
处理用户上传的 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 特性”更容易审计;如果业务确实需要链接,应先把链接目标解析为根目录内的普通路径,再单独实现。

用规范化路径和相对路径确认不会越界
不要只用 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.Typeflag 和 Header.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/tar、fmt、io 和 path/filepath。示例中的 io.CopyN 会严格按头部声明的长度复制当前条目;下一次 Next 会处理 tar Reader 余下的填充。生产环境可以把总条目数、总展开字节数和单层目录深度也设为上限,防止大量小文件拖垮 inode。

把 ErrInsecurePath 和业务检查放进上线清单
| 检查项 | 处理方式 | 不能忽略的原因 |
|---|---|---|
绝对路径或 ../ | 拒绝并记录原始条目名 | 避免目录穿越 |
| 符号链接、硬链接、设备节点 | 默认拒绝 | Header.Name 安全不代表 Linkname 安全 |
| 单项与总展开大小 | 在创建文件前检查 | 防止磁盘和内存资源被耗尽 |
ErrInsecurePath | 按业务策略拒绝,不直接忽略 | 库提示不是完整的链接安全策略 |
如果解压根目录是临时目录,创建后应限制权限,并在任务成功或失败时统一清理。不要因为 Next 能够读出条目,就认为它适合直接写盘;读取格式和落盘授权是两个边界。
常见问题
只调用 filepath.Clean 能防止 tar 目录穿越吗?
不能。清理路径后仍需用 Rel 或等价的边界判断确认目标位于根目录内,并单独处理链接条目。
为什么不直接跟随 tar 中的符号链接?
因为链接目标可能指向根目录外,甚至指向设备或敏感文件。除非有明确需求并完成目标解析与边界检查,否则拒绝最容易保持安全。
设置 tarinsecurepath=0 后还需要自己校验吗?
需要。官方文档说明该机制只验证文件名,而且依赖运行时设置;应用自己的类型、大小、总量和根目录策略不能省略。
Redis Hash与JSON字符串存储对象的选择边界
- 上一篇
- Redis Hash与JSON字符串存储对象的选择边界
- 下一篇
- 商汤Seko短视频创作者使用指南:从准备素材到导出结果
-
- Golang · Go教程 | 13分钟前 | go · net/http ·
- Go http.Transport空闲连接失效时的回收参数配置
- 185浏览 收藏
-
- Golang · Go教程 | 28分钟前 | Go教程 · net/http · CheckRedirect Go http.Client重定向 ErrUseLastResponse HTTP跳转策略
- Go http.Client自定义重定向策略的配置方法
- 246浏览 收藏
-
- Golang · Go教程 | 38分钟前 | golang · net/url url.Values QueryEscape 签名查询串
- Go net/url构造签名查询串时保持参数编码一致的方法
- 313浏览 收藏
-
- Golang · Go教程 | 52分钟前 |
- Go regexp命名分组提取日志字段的维护方式
- 354浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go gzip.Writer写入HTTP响应时的刷新时机设计
- 108浏览 收藏
-
- Golang · Go教程 | 1小时前 | 文件处理 · Go教程 · zip Go 流式读取 内存限制 archive/zip
- Go archive/zip逐项读取压缩包并控制内存占用的方法
- 358浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go io/fs.ValidPath校验虚拟文件路径的使用边界
- 380浏览 收藏
-
- Golang · Go教程 | 2小时前 | go · 性能 · 文件系统 · path/filepath filepath.WalkDir Go目录遍历
- Go filepath.WalkDir按目录深度限制大型仓库扫描范围的实现
- 336浏览 收藏
-
- Golang · Go教程 | 2小时前 | go ·
- Go os.Open读取配置后保证句柄关闭的结构化写法
- 410浏览 收藏
-
- Golang · Go教程 | 2小时前 | bytes.Buffer · Go教程 · http.MaxBytesReader Go bytes.Buffer容量上限 Go请求体限制 bytes.Buffer Grow
- Go bytes.Buffer设置最大容量防止请求体膨胀的处理方案
- 390浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go io.Pipe连接压缩器与上传器的背压处理方案
- 295浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 133次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 200次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 146次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 124次使用
-
- CMMLU
- 深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
- 110次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览

