package artifact import ( "archive/tar" "context" "fmt" "io" "os" "path/filepath" "strings" "github.com/klauspost/compress/zstd" ) // ExtractZstdTar decompresses a .tar.zst archive into targetDir, streaming // through the reader without buffering the full archive. Refuses entries // with ".." in the path or absolute paths (tar-slip defense). func ExtractZstdTar(ctx context.Context, archivePath, targetDir string) error { f, err := os.Open(archivePath) if err != nil { return fmt.Errorf("open archive: %w", err) } defer f.Close() zr, err := zstd.NewReader(f) if err != nil { return fmt.Errorf("zstd reader: %w", err) } defer zr.Close() tr := tar.NewReader(zr) if err := os.MkdirAll(targetDir, 0o755); err != nil { return fmt.Errorf("mkdir target: %w", err) } cleanTarget, err := filepath.Abs(targetDir) if err != nil { return err } for { if err := ctx.Err(); err != nil { return err } hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return fmt.Errorf("tar next: %w", err) } // tar-slip defense clean := filepath.Clean(hdr.Name) if strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) { return fmt.Errorf("refusing suspicious archive path: %s", hdr.Name) } outPath := filepath.Join(cleanTarget, clean) if !strings.HasPrefix(filepath.Clean(outPath)+string(os.PathSeparator), cleanTarget+string(os.PathSeparator)) && filepath.Clean(outPath) != cleanTarget { return fmt.Errorf("refusing path outside target: %s", hdr.Name) } switch hdr.Typeflag { case tar.TypeDir: if err := os.MkdirAll(outPath, os.FileMode(hdr.Mode)); err != nil { return err } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { return err } out, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)) if err != nil { return fmt.Errorf("create %s: %w", outPath, err) } if _, err := io.Copy(out, tr); err != nil { out.Close() return fmt.Errorf("write %s: %w", outPath, err) } if err := out.Close(); err != nil { return err } case tar.TypeSymlink, tar.TypeLink: // Refuse links for safety — a Mithril archive has no legitimate reason to contain them. return fmt.Errorf("refusing link entry: %s", hdr.Name) default: // Silently skip unknown types. } } return nil }