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 特性”更容易审计;如果业务确实需要链接,应先把链接目标解析为根目录内的普通路径,再单独实现。

用规范化路径和相对路径确认不会越界
不要只用 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 后还需要自己校验吗?
需要。官方文档说明该机制只验证文件名,而且依赖运行时设置;应用自己的类型、大小、总量和根目录策略不能省略。
-
185 收藏
-
Golang · Go教程 | 24分钟前 | Go教程 · net/http · CheckRedirect Go http.Client重定向 ErrUseLastResponse HTTP跳转策略246 收藏
-
313 收藏
-
354 收藏
-
108 收藏
-
358 收藏
-
145 收藏
-
380 收藏
-
336 收藏
-
410 收藏
-
Golang · Go教程 | 2小时前 | bytes.Buffer · Go教程 · http.MaxBytesReader Go bytes.Buffer容量上限 Go请求体限制 bytes.Buffer Grow390 收藏
-
295 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习