Skip to content

Commit

Permalink
Fixed path related issues
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasdille committed Dec 14, 2023
1 parent 0b35ceb commit 829749c
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 35 deletions.
13 changes: 12 additions & 1 deletion cmd/uniget/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,14 @@ func installTools(requestedTools tool.Tools, check bool, plan bool, reinstall bo
}

assertDirectory(prefix + "/" + target)
err := tool.Install(registryImagePrefix, prefix, target, altArch)
var err error
if user {
logging.Debug.Printfln("Installing in user context")
err = tool.Install(registryImagePrefix, prefix+"/"+target, "", altArch)
} else {
logging.Debug.Printfln("Installing in system context")
err = tool.Install(registryImagePrefix, prefix, target, altArch)
}
if err != nil {
logging.Warning.Printfln("Unable to install %s: %s", tool.Name, err)
continue
Expand All @@ -347,6 +354,10 @@ func installTools(requestedTools tool.Tools, check bool, plan bool, reinstall bo
}
}

if user {
logging.Warning.Printfln("Post installation is not yet support for user context")
return nil
}
if len(prefix) > 0 {
logging.Warning.Printfln("Post installation skipped because prefix is set to %s", prefix)
logging.Warning.Printfln("Please run 'uniget postinstall' in the context of %s to complete the installation", prefix)
Expand Down
7 changes: 4 additions & 3 deletions cmd/uniget/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,11 @@ func main() {
}

if user {
target = os.Getenv("HOME") + "/.local"
cacheRoot = os.Getenv("HOME") + "/.cache"
prefix = os.Getenv("HOME")
target = ".local"
cacheRoot = ".cache"
cacheDirectory = cacheRoot + "/" + projectName
libRoot = os.Getenv("HOME") + "/.local/state"
libRoot = ".local/state"
libDirectory = libRoot + "/" + projectName
metadataFile = cacheDirectory + "/" + metadataFileName

Expand Down
8 changes: 4 additions & 4 deletions cmd/uniget/postinstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,21 @@ func postinstall() error {
if err != nil {
return fmt.Errorf("unable to read post_install directory: %s", err)
}
infos := make([]fs.FileInfo, 0, len(entries))
scripts := make([]fs.FileInfo, 0, len(entries))
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
return fmt.Errorf("unable to get info for %s: %s", entry.Name(), err)
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".sh") {
infos = append(infos, info)
scripts = append(scripts, info)
}
}
if len(infos) > 0 && len(prefix) > 0 {
if len(scripts) > 0 && len(prefix) > 0 {
pterm.Warning.Printfln("prefix cannot be set for postinstall scripts to run")
return nil
}
for _, file := range infos {
for _, file := range scripts {
logging.Info.Printfln("Running post_install script %s", file.Name())

logging.Debug.Printfln("Running pre_install script %s", "/"+libDirectory+"/pre_install/"+file.Name())
Expand Down
70 changes: 49 additions & 21 deletions pkg/archive/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
func pathIsInsideTarget(target string, candidate string) error {
absTarget, err := filepath.Abs(target)
if err != nil {
return fmt.Errorf("ExtractTarGz: Abs() failed: %s", err.Error())
return fmt.Errorf("pathIsInsideTarget(): Abs() failed: %s", err.Error())
}

log.Debugf("Checking if %s works\n", filepath.Join(absTarget, candidate))
Expand All @@ -29,18 +29,18 @@ func pathIsInsideTarget(target string, candidate string) error {
realPath = cleanPath

} else if err != nil {
return fmt.Errorf("ExtractTarGz: EvalSymlinks() failed: %s", err.Error())
return fmt.Errorf("pathIsInsideTarget(): EvalSymlinks() failed: %s", err.Error())
}
log.Debugf("Realpath of %s is %s\n", candidate, realPath)

relPath, err := filepath.Rel(absTarget, realPath)
if err != nil {
return fmt.Errorf("ExtractTarGz: Rel() failed: %s", err.Error())
return fmt.Errorf("pathIsInsideTarget(): Rel() failed: %s", err.Error())
}
log.Debugf("Relative path of %s is %s\n", realPath, relPath)

if strings.Contains(relPath, "..") {
return fmt.Errorf("ExtractTarGz: symlink target contains '..': %s", relPath)
return fmt.Errorf("pathIsInsideTarget(): symlink target contains '..': %s", relPath)
}

return nil
Expand All @@ -58,20 +58,25 @@ func ExtractTarGz(gzipStream io.Reader, patchPath func(path string) string) erro

for {
header, err := tarReader.Next()

if err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("ExtractTarGz: Next() failed: %s", err.Error())
}

if err != nil {
return fmt.Errorf("ExtractTarGz: Next() failed: %s", err.Error())
// Directories will be created when files/(sym)links are unpacked
if header.Typeflag == tar.TypeDir {
continue
}

// Prevent path traversal attacks using ..
if strings.Contains(header.Name, "..") {
return fmt.Errorf("ExtractTarGz: filename contains '..': %s", header.Name)
}

log.Tracef("Processing %s\n", header.Name)

// Use callback function to patch path
fixedHeaderName := patchPath(header.Name)
log.Tracef(" Stripped name is %s\n", fixedHeaderName)
if len(fixedHeaderName) == 0 {
Expand All @@ -80,48 +85,55 @@ func ExtractTarGz(gzipStream io.Reader, patchPath func(path string) string) erro
}

switch header.Typeflag {
case tar.TypeDir:
log.Tracef("Creating directory %s\n", fixedHeaderName)
_, err := os.Stat(fixedHeaderName)
if err != nil {
err := os.Mkdir(fixedHeaderName, 0755) // #nosec G301 -- Tools must be world readable
if err != nil {
return fmt.Errorf("ExtractTarGz: Mkdir() failed: %s", err.Error())
}
}

// Unpack file
case tar.TypeReg:
log.Tracef("Untarring file %s\n", fixedHeaderName)

// Prevent path traversal attacks using absolute paths
cleanFixedHeaderName := filepath.Clean(fixedHeaderName)
if strings.HasPrefix(cleanFixedHeaderName, "/") {
return fmt.Errorf("ExtractTarGz: filename starts with '/': %s", cleanFixedHeaderName)
}

// Create directories for file
dir := filepath.Dir(fixedHeaderName)
err := os.MkdirAll(dir, 0755) // #nosec G301 -- Tools must be world readable
if err != nil {
return fmt.Errorf("ExtractTarGz: MkdirAll() failed: %s", err.Error())
}

// Create file
outFile, err := os.Create(fixedHeaderName)
if err != nil {
return fmt.Errorf("ExtractTarGz: Create() failed: %s", err.Error())
}
defer outFile.Close()
// Write contents
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("ExtractTarGz: Copy() failed: %s", err.Error())
} // #nosec G110 -- Tool images are a trusted source
// Set permissions
mode := os.FileMode(header.Mode)
err = outFile.Chmod(mode)
if err != nil {
return fmt.Errorf("ExtractTarGz: Chmod() failed: %s", err.Error())
}
err = outFile.Close()
if err != nil {
return fmt.Errorf("ExtractTarGz: Failed to close %s: %s", fixedHeaderName, err.Error())
}

// Unpack symlink
case tar.TypeSymlink:
log.Tracef("Untarring symlink %s\n", fixedHeaderName)

// Check if symlink already exists
_, err := os.Stat(fixedHeaderName)
if err == nil {
log.Debugf("Symlink %s already exists\n", fixedHeaderName)
}
// Continue if symlink does not exist
if os.IsNotExist(err) {
log.Debugf("Symlink %s does not exist\n", fixedHeaderName)

// Prevent path traversal attacks for symlink source and target
absHeaderLinkname := header.Linkname
if !filepath.IsAbs(header.Linkname) {
absHeaderLinkname = filepath.Join(filepath.Dir(fixedHeaderName), header.Linkname) // #nosec G305 -- Following code prevents traversal
Expand All @@ -131,37 +143,53 @@ func ExtractTarGz(gzipStream io.Reader, patchPath func(path string) string) erro
if err != nil {
return fmt.Errorf("ExtractTarGz: pathIsInsideTarget() failed for %s: %s", absHeaderLinkname, err.Error())
}

err = pathIsInsideTarget(target, fixedHeaderName)
if err != nil {
return fmt.Errorf("ExtractTarGz: pathIsInsideTarget() failed for %s: %s", fixedHeaderName, err.Error())
}

// Create directories for symlink
dir := filepath.Dir(fixedHeaderName)
log.Tracef("Creating directory %s\n", dir)
err := os.MkdirAll(dir, 0755) // #nosec G301 -- Tools must be world readable
if err != nil {
return fmt.Errorf("ExtractTarGz: MkdirAll() failed: %s", err.Error())
}

// Create symlink
err = os.Symlink(header.Linkname, fixedHeaderName)
if err != nil {
return fmt.Errorf("ExtractTarGz: Symlink() failed: %s", err.Error())
}
}

// Unpack hardlink
case tar.TypeLink:
log.Tracef("Untarring link %s\n", fixedHeaderName)

// Check if link already exists
_, err := os.Stat(fixedHeaderName)
if err == nil {
log.Debugf("Link %s already exists\n", fixedHeaderName)
}
// Continue if link does not exist
if os.IsNotExist(err) {
log.Debugf("Target of link %s does not exist\n", fixedHeaderName)

// Remove existing link
err = os.Remove(fixedHeaderName)
if err != nil {
return fmt.Errorf("ExtractTarGz: Remove() failed for TypeLink: %s", err.Error())
}

// Create link
err = os.Link(header.Linkname, fixedHeaderName)
if err != nil {
return fmt.Errorf("ExtractTarGz: Link() failed: %s", err.Error())
}
}

// Fail on unhandled types
default:
return fmt.Errorf("ExtractTarGz: unknown type for entry %s: %b", header.Name, header.Typeflag)
}
Expand Down
36 changes: 30 additions & 6 deletions pkg/tool/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,52 @@ import (
)

func (tool *Tool) Install(registryImagePrefix string, prefix string, target string, altArch string) error {
// Fetch manifest for tool
err := containers.GetManifest(fmt.Sprintf(registryImagePrefix+"%s:%s", tool.Name, strings.Replace(tool.Version, "+", "-", -1)), altArch, func(blob blob.Reader) error {
pterm.Debug.Printfln("Extracting with prefix=%s and target=%s", prefix, target)
if len(prefix) > 0 {
err := os.Chdir(prefix)
if err != nil {
return fmt.Errorf("error changing directory to %s: %s", prefix, err)
}

// Change working directory to prefix
// so that unpacking can ignore the target directory
installDir := prefix
if len(prefix) == 0 {
installDir = "/"
}
err := os.Chdir(installDir)
if err != nil {
return fmt.Errorf("error changing directory to %s: %s", prefix, err)
}
dir, err := os.Getwd()
if err != nil {
return fmt.Errorf("error getting working directory")
}
pterm.Debug.Printfln("Current directory: %s", dir)

// Unpack tool
err = archive.ExtractTarGz(blob, func(path string) string {
// Skip paths that are a prefix of usr/local/
// Necessary as long as tools are still installed in hardcoded /usr/local
if strings.HasPrefix("usr/local/", path) {
pterm.Debug.Println("Path is prefix of usr/local/")
return ""
}

// Remove prefix usr/local/ to support arbitrary target directories
fixedPath := strings.TrimPrefix(path, "usr/local/")

pterm.Debug.Printfln("fixedPath=%s", fixedPath)
pterm.Debug.Printfln(" 012345678901234567890")
if len(fixedPath) >= 16 {
pterm.Debug.Printfln(" %s", fixedPath[0:15])
}

// Prepend target directory for all paths except those with prefix var/lib/uniget/
if len(fixedPath) >= 16 && fixedPath[0:15] == "var/lib/uniget/" {
pterm.Debug.Printfln("No need to prepend target")
} else {
} else if len(target) > 0 {
pterm.Debug.Printfln("Prepending target to %s", fixedPath)
fixedPath = target + "/" + fixedPath
}

return fixedPath
})
if err != nil {
Expand All @@ -55,15 +75,19 @@ func (tool *Tool) Install(registryImagePrefix string, prefix string, target stri
}

func (tool *Tool) Inspect(registryImagePrefix string, altArch string) error {
// Fetch manifest for tool
err := containers.GetManifest(fmt.Sprintf(registryImagePrefix+"%s:%s", tool.Name, strings.Replace(tool.Version, "+", "-", -1)), altArch, func(blob blob.Reader) error {
result, err := archive.ListTarGz(blob, func(path string) string {
// Remove prefix usr/local/ to support arbitrary target directories
// Necessary as long as tools are still installed in hardcoded /usr/local
fixedPath := strings.TrimPrefix(path, "usr/local/")
return fixedPath
})
if err != nil {
return fmt.Errorf("failed to extract layer: %s", err)
}

// Display contents of tool image
for _, file := range result {
fmt.Printf("%s\n", file)
}
Expand Down

0 comments on commit 829749c

Please sign in to comment.