Go 解压 tar 时怎样避免路径穿越
来源:17golang原创
时间:2026-11-14 15:35:44 375浏览 收藏
解压不可信 tar 时,最容易写错的地方不是 tar.NewReader,而是把 Header.Name 直接交给 filepath.Join,然后默认“拼接出来的路径还在目标目录里”。可靠的做法是:先用 filepath.IsLocal 拒绝非本地名称,再只接受普通文件和目录,最后把写入限制在新建的目标根目录中;符号链接和硬链接一律不要按归档内容创建。
一句话结论:Header.Name负责描述归档成员,不是可信的文件系统路径;先验证它,再决定是否落盘,且不要让Header.Linkname参与写入路径。
- 文件名层:拒绝绝对路径、空名称和包含
..的非本地路径。 - 条目层:只允许普通文件、目录,拒绝符号链接、硬链接、设备文件和 FIFO。
- 根目录层:使用专用空目录或
os.Root,再叠加单文件与总量上限。
先拆开 Header.Name、目标根目录和链接目标
我在处理上传包时,通常先把风险拆成三个字段看。Header.Name 是归档中的成员名,可能是 docs/readme.md,也可能是绝对路径或带父目录跳转的名称;Header.Linkname 是链接条目的目标,它和当前文件名是两套语义;目标根目录则是程序真正允许写入的边界。
只检查 Header.Name 不够:一个看似正常的链接条目可以把后续写入导向根目录之外。反过来,只调用 filepath.Clean 也不够,因为 Clean 只是词法整理,不能证明结果没有越过边界。下面这张结构图把三个边界放在一起,先看清谁能影响写入,再看代码。

用 filepath.IsLocal 拒绝越界文件名
Go 的 filepath.IsLocal 是一个适合做第一道门的词法检查:它拒绝绝对路径、空路径、会走出当前子树的路径,并考虑 Windows 保留设备名。返回 true 后,filepath.Join(root, name) 才具有“仍在 root 下”的词法保证。
注意它不检查已经存在的符号链接,也不检查另一个进程是否会在检查和写入之间替换目录。因此它解决的是归档名称问题,不是完整的文件系统竞态问题。
func localTarget(root, name string) (string, error) {
// Header.Name 来自归档,先做词法检查,再参与 Join。
if !filepath.IsLocal(name) {
return "", fmt.Errorf("拒绝非本地归档名称 %q", name)
}
// IsLocal 成功后,Join 的结果不会靠 .. 逃出 root。
return filepath.Join(root, name), nil
}
如果运行环境已经支持较新的 archive/tar 行为,也可以把 GODEBUG=tarinsecurepath=0 作为辅助保护。此时 Reader.Next 会对非本地文件名返回 tar.ErrInsecurePath,但官方说明仍然强调:它只校验文件名,不校验链接目标。应用代码仍应显式检查错误并拒绝该条目。
只落盘普通文件和目录

安全解压最简单的白名单是:目录用于创建目录,普通文件用于写入,其他类型直接拒绝。TypeSymlink 与 TypeLink 都不应该进入普通写入分支;设备文件和 FIFO 也没有必要由一个上传解包器创建。
func extractTar(r io.Reader, root string, maxFileSize int64) error {
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
// 归档正常结束。
return nil
}
if err != nil {
return fmt.Errorf("读取 tar 头失败: %w", err)
}
if errors.Is(err, tar.ErrInsecurePath) || !filepath.IsLocal(hdr.Name) {
return fmt.Errorf("拒绝不安全名称 %q", hdr.Name)
}
target := filepath.Join(root, hdr.Name)
switch hdr.Typeflag {
case tar.TypeDir:
// 目录权限由服务统一控制,不直接信任归档中的 Mode。
if err := os.MkdirAll(target, 0o755); err != nil {
return fmt.Errorf("创建目录 %q 失败: %w", hdr.Name, err)
}
case tar.TypeReg, tar.TypeRegA:
// 先限制声明大小,避免把超大成员完整写入磁盘。
if hdr.Size maxFileSize {
return fmt.Errorf("文件 %q 超过大小上限", hdr.Name)
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return fmt.Errorf("创建父目录失败: %w", err)
}
// O_EXCL 防止归档条目静默覆盖已存在文件。
f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
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:
// 不创建链接、设备和 FIFO,避免 Linkname 改写后续写入边界。
return fmt.Errorf("拒绝归档类型 %d: %q", hdr.Typeflag, hdr.Name)
}
}
}
这段代码假定 root 是本次解压新建的专用目录,并且没有不可信进程同时修改它。若目录是共享的,MkdirAll 仍可能遇到预先放置的符号链接;这时不能把词法检查当成文件系统级隔离。
把目标根目录当成真正的安全边界
在可控的临时目录中解压,最实用的做法是每个任务新建一个空目录,任务结束后整体移交或删除。这样即便归档里有重复条目,也不会覆盖宿主机已有配置。
如果目标目录会被其他用户或进程写入,Go 1.24 起可以关注 os.OpenRoot、Root.OpenFile 等根目录级 API;Go 1.25 起还提供了 Root.MkdirAll。它们让文件名解析和符号链接检查绑定在打开的根目录上。升级时仍要看目标 Go 版本的安全公告和平台限制,不能把 API 名称当成自动完成的权限沙箱。
另外,路径穿越只是解包风险的一部分。生产代码还应设置归档条目数量、单文件大小和总解压大小上限;这些限制解决资源耗尽问题,和 Header.Name 的路径边界是两条独立的防线。
发布前按这份清单收口
- 是否在
filepath.Join之前检查了filepath.IsLocal(hdr.Name)? - 是否把
TypeSymlink、TypeLink、设备文件和 FIFO 排除在白名单外? - 是否把解压结果写入新建的专用目录,而不是应用目录或系统目录?
- 是否避免信任
Header.Mode,并使用O_EXCL防止静默覆盖? - 共享目录是否改用根目录级文件 API,并评估并发替换符号链接的风险?
- 是否限制条目数、单文件大小和总输出大小?
记住这条边界就够了:tar 里的名称和链接都是输入数据,不是授权。先让名称保持本地,再让条目类型保持简单,最后让文件系统操作保持在明确的根目录内,解压器才有可审计的安全边界。
常见问题
只判断字符串是否以目标目录开头可以吗?不建议。前缀比较没有路径边界概念,还会遇到大小写、分隔符和符号链接问题;至少使用 filepath.IsLocal,并把写入放进专用目录。
为什么不直接把符号链接还原出来?因为链接目标来自归档作者,恢复它会把后续写入交给链接解析。除非你有专门的文件系统隔离和链接策略,否则解压服务应直接拒绝链接条目。
-
339 收藏
-
388 收藏
-
253 收藏
-
182 收藏
-
374 收藏
-
Golang · Go问答 | 14分钟前 | 错误处理 · HTTP · 文件上传 · Go问答 · 文件上传 Go FileHeader http.Request.FormFile multipart.File ErrMissingFile451 收藏
-
309 收藏
-
431 收藏
-
322 收藏
-
257 收藏
-
437 收藏
-
Golang · Go问答 | 33分钟前 | 标准库 · Go问答 · 协议解析 net/textproto Go textproto.Reader.ReadContinuedLineBytes 折行头部 Go 字节读取499 收藏
-
Golang · Go问答 | 40分钟前 | 标准库 · HTTP · Cookie · 故障排查 · Go问答 · Go net/http Cookiejar Set-Cookie ParseSetCookie Response.Cookies293 收藏
-
Golang · Go问答 | 44分钟前 | pprof · 性能排查 · Go问答 · runtime/pprof Go runtime.SetBlockProfileRate block profile 阻塞采样率 Go 性能排查185 收藏
-
Golang · Go问答 | 50分钟前 | server · HTTP服务 · net/http · Go问答 · keep-alive ReadTimeout WriteTimeout IdleTimeout HTTP 超时 Go http.Server283 收藏
-
441 收藏
-
452 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习