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

Go archive/tar 怎样防止路径穿越

来源:17golang原创

时间:2026-10-14 02:42:53 230浏览 收藏

处理用户上传的 tar 包时,真正危险的不是 tar.Reader 会不会读取,而是 Header.Name 会把文件写到哪里。安全做法是:读取每个条目后先用 filepath.IsLocal 拒绝空路径、绝对路径和包含 .. 的路径,再把结果拼到固定目标目录;目录和普通文件分别处理,符号链接、硬链接直接拒绝。这样才不会把解包目标变成攻击者控制的任意写入点。

关键原则:归档成员名只允许“目标目录内部的本地相对路径”,并且路径检查、条目类型判断和文件创建要在同一段解包逻辑里完成。

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

这篇文章用一个可落地的解包骨架说明边界。代码采用 Go 标准库接口,图片是静态结构说明图,不是运行截图或执行证据。

先把归档路径限制在目标目录内

常见错误是直接写出 filepath.Join(dst, hdr.Name),然后把“拼接成功”误认为“路径安全”。Join 会清理路径,但它不会替你决定归档成员是否应该被接受;输入仍应先经过本地相对路径判断。

package main

import (
	"fmt"
	"path/filepath"
)

// safeTarget 只接受目标目录内部的本地相对路径。
// filepath.IsLocal 采用词法判断,可拦截空路径、绝对路径和 .. 段。
func safeTarget(dst, name string) (string, error) {
	if !filepath.IsLocal(name) {
		return "", fmt.Errorf("拒绝不安全归档路径: %q", name)
	}
	return filepath.Join(dst, name), nil
}

例如 docs/readme.txt 可以继续处理,而 ../outside.txt、/tmp/outside.txt 和空名称都会被拒绝。这里的“本地”是词法意义上的本地,后面还要处理链接条目,因为路径字符串本身安全,并不等于文件系统访问绝对安全。

归档成员名经过 filepath.IsLocal 后进入固定目标目录的静态边界关系图
图1:路径边界说明图,展示 Header.Name、filepath.IsLocal、filepath.Join 与目标目录之间的静态关系。

读取 Header 时不要忽略 ErrInsecurePath

tar.Reader.Next 每次返回一个条目头,并把当前条目的数据暴露为 io.Reader。在支持该行为的 Go 环境中,如果设置了 GODEBUG=tarinsecurepath=0,非本地文件名会伴随 tar.ErrInsecurePath 返回。业务解包器不应该把这个错误当成普通提示继续写入。

for {
	hdr, err := tr.Next()
	if err == io.EOF {
		break // 归档正常结束。
	}
	if err != nil {
		// ErrInsecurePath 表示成员名越过了本地路径边界,必须拒绝。
		name := ""
		if hdr != nil {
			name = hdr.Name // 保留服务端日志中的归档成员名。
		}
		return fmt.Errorf("读取归档条目 %q: %w", name, err)
	}
	// hdr.Name 后面还要做 IsLocal 检查,不能只依赖 GODEBUG。
	// 这里进入条目类型分派逻辑。
}

更稳妥的做法是无论运行时开关怎样,都显式调用 filepath.IsLocal。这样代码的安全边界写在应用逻辑里,不依赖部署环境是否恰好带上某个 GODEBUG 值。

目录、普通文件和链接必须分开处理

条目类型决定了写入动作。目录只需要创建;普通文件才可以创建文件并复制数据;符号链接和硬链接不能沿用普通文件分支。官方文档特别提醒,Next 的不安全路径判断只针对文件名,不会校验链接目标,因此不能只检查 hdr.Name。

func extractEntry(tr *tar.Reader, hdr *tar.Header, dst string) error {
	target, err := safeTarget(dst, hdr.Name)
	if err != nil {
		return err
	}

	switch hdr.Typeflag {
	case tar.TypeDir:
		// 目录沿着归档路径创建,并限制为 owner 可读写可进入。
		return os.MkdirAll(target, 0700)
	case tar.TypeReg, tar.TypeRegA, 0:
		// 普通文件先创建父目录,再复制 Header.Size 对应的数据。
		if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
			return err
		}
		f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
		if err != nil {
			return err
		}
		_, copyErr := io.CopyN(f, tr, hdr.Size)
		closeErr := f.Close() // 关闭错误也要保留,避免静默丢数据。
		if copyErr != nil {
			return copyErr
		}
		return closeErr
	case tar.TypeSymlink, tar.TypeLink:
		// 链接目标可能跳出目标目录;当前策略直接拒绝。
		return fmt.Errorf("拒绝链接条目: %q", hdr.Name)
	default:
		// 设备、FIFO 等特殊条目不在普通解包范围内。
		return fmt.Errorf("拒绝特殊条目 %q,类型 %d", hdr.Name, hdr.Typeflag)
	}
}

上面的分派逻辑故意不调用 os.Symlink 或 os.Link。如果业务确实需要保留链接,必须额外约束 hdr.Linkname、解析链接后的真实路径并处理竞态;在一般上传解包场景里,拒绝链接的边界更清楚。

tar Header 类型分派到目录、普通文件或拒绝链接分支的静态结构图
图2:条目类型说明图,展示 Header.Typeflag 与目录、普通文件、链接拒绝边界之间的静态关系。

文件创建和数据复制还要守住最后一层

安全路径判断解决的是“写到哪里”,并不自动解决“写多少”和“写完没有”。应使用 io.CopyN 按 hdr.Size 复制当前条目数据,检查复制错误和关闭错误,并在生产代码中增加单文件大小、总解包大小和条目数量上限,避免合法路径上的资源消耗型归档。

还要记住 filepath.IsLocal 是纯词法操作,不会考虑目标目录中已经存在的符号链接。如果解包目录可能被其他进程或不可信用户修改,单纯的路径拼接仍不足够,应把目标目录隔离到专用工作区,并限制该目录的写权限;更高要求的场景需要使用平台提供的目录句柄相对打开能力或沙箱。

最后可以按下面的清单复查实现:成员名先过 IsLocal;ErrInsecurePath 不被忽略;链接条目默认拒绝;父目录权限明确;普通文件限制复制长度;CopyN 和 Close 错误都返回;单文件、总大小和条目数量有上限。做到这些,archive/tar 的读取能力才不会被误用成任意路径写入器。

相关问题

  • 只调用 filepath.Clean 能防止路径穿越吗?不能。Clean 只做词法清理,仍应判断结果是否为本地相对路径。
  • 为什么不建议直接恢复 tar 中的符号链接?因为 Header.Name 安全不代表 Header.Linkname 安全,链接可能把写入或后续访问引向目标目录之外。
  • tar.Reader 会自动限制文件大小吗?它按 Header.Size 提供当前条目数据,业务仍应设置单文件和总解包大小上限。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>