Files
goget/internal/archive/tar.go
T

135 lines
3.4 KiB
Go

//go:build linux || freebsd
// +build linux freebsd
package archive
import (
"archive/tar"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
type tarExtractor struct{ *BaseExtractor }
func NewTarExtractor() *tarExtractor {
return &tarExtractor{NewBaseExtractor("tar", []string{".tar"})}
}
// Extract extracts a tar archive into destDir (streaming-friendly)
func (t *tarExtractor) Extract(r io.Reader, destDir string) error {
tr := tar.NewReader(r)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("failed to read tar header: %w", err)
}
if err := t.extractEntry(header, tr, destDir); err != nil {
return err
}
}
return nil
}
// extractEntry extracts a single entry from a tar archive
func (t *tarExtractor) extractEntry(header *tar.Header, r io.Reader, destDir string) error {
// Sanitize path to prevent tar-slip attacks
cleanName := filepath.Clean(header.Name)
if strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
return fmt.Errorf("unsafe path in tar: %s", header.Name)
}
targetPath := filepath.Join(destDir, cleanName)
switch header.Typeflag {
case tar.TypeDir:
// Create directory with original permissions
return os.MkdirAll(targetPath, os.FileMode(header.Mode))
case tar.TypeReg:
// Create parent directories
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
// Create file with original permissions (mask setuid/setgid/sticky)
mode := os.FileMode(header.Mode) & 0777
file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
defer file.Close()
// Copy contents
if _, err := io.Copy(file, r); err != nil {
return err
}
// Restore timestamps
atime := header.AccessTime
if atime.IsZero() {
atime = time.Now()
}
mtime := header.ModTime
if mtime.IsZero() {
mtime = time.Now()
}
if err := os.Chtimes(targetPath, atime, mtime); err != nil {
// Non-fatal
}
return nil
case tar.TypeSymlink:
// Validate symlink target against path traversal
linkTarget := filepath.Clean(header.Linkname)
if filepath.IsAbs(linkTarget) || strings.HasPrefix(linkTarget, "..") {
return fmt.Errorf("symlink target outside extraction directory: %s", header.Linkname)
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
os.Remove(targetPath)
return os.Symlink(linkTarget, targetPath)
case tar.TypeLink:
// Hard link - validate target stays within destDir to prevent path traversal
cleanTarget := filepath.Clean(header.Linkname)
if filepath.IsAbs(cleanTarget) || strings.HasPrefix(cleanTarget, "..") {
return fmt.Errorf("hard link target outside extraction directory: %s", header.Linkname)
}
linkTarget := filepath.Join(destDir, cleanTarget)
// Ensure resolved path does not escape destDir
if !strings.HasPrefix(linkTarget, destDir+string(filepath.Separator)) && linkTarget != destDir {
return fmt.Errorf("hard link target outside extraction directory: %s", header.Linkname)
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
return os.Link(linkTarget, targetPath)
case tar.TypeChar, tar.TypeBlock:
// Device files - skip with warning (requires root)
return nil
case tar.TypeFifo:
// FIFO - skip (platform-specific)
return nil
default:
// Unknown type - skip
return nil
}
}
func init() { Register(NewTarExtractor()) }