Harden Windows package paths (T-605)
This commit is contained in:
+72
-47
@@ -7,10 +7,10 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"softbox.local/core/internal/safepath"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -45,6 +45,7 @@ type plannedEntry struct {
|
||||
file *zip.File
|
||||
archivePath string
|
||||
outputPath string
|
||||
targetPath string
|
||||
directory bool
|
||||
}
|
||||
|
||||
@@ -85,11 +86,19 @@ func (extractor Extractor) extract(
|
||||
if err != nil {
|
||||
return ExtractResult{}, err
|
||||
}
|
||||
destinationRoot, entrypointPath, err := planOutputPaths(
|
||||
destination,
|
||||
normalizedEntrypoint,
|
||||
plan,
|
||||
)
|
||||
if err != nil {
|
||||
return ExtractResult{}, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(destinationRoot), 0o700); err != nil {
|
||||
return ExtractResult{}, fmt.Errorf("create staging parent: %w", err)
|
||||
}
|
||||
if err := os.Mkdir(destination, 0o700); err != nil {
|
||||
if err := os.Mkdir(destinationRoot, 0o700); err != nil {
|
||||
if os.IsExist(err) {
|
||||
return ExtractResult{}, ErrDestinationExists
|
||||
}
|
||||
@@ -98,23 +107,22 @@ func (extractor Extractor) extract(
|
||||
complete := false
|
||||
defer func() {
|
||||
if !complete {
|
||||
_ = os.RemoveAll(destination)
|
||||
_ = os.RemoveAll(destinationRoot)
|
||||
}
|
||||
}()
|
||||
|
||||
var written int64
|
||||
for _, entry := range plan {
|
||||
target := filepath.Join(destination, filepath.FromSlash(entry.outputPath))
|
||||
if entry.directory {
|
||||
if entry.outputPath == "" {
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(target, 0o700); err != nil {
|
||||
if err := os.MkdirAll(entry.targetPath, 0o700); err != nil {
|
||||
return ExtractResult{}, fmt.Errorf("create staging directory: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(entry.targetPath), 0o700); err != nil {
|
||||
return ExtractResult{}, fmt.Errorf("create staging file parent: %w", err)
|
||||
}
|
||||
|
||||
@@ -126,7 +134,11 @@ func (extractor Extractor) extract(
|
||||
if entry.file.Mode().Perm()&0o111 != 0 {
|
||||
mode = 0o700
|
||||
}
|
||||
output, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
|
||||
output, err := os.OpenFile(
|
||||
entry.targetPath,
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
||||
mode,
|
||||
)
|
||||
if err != nil {
|
||||
source.Close()
|
||||
return ExtractResult{}, fmt.Errorf("create staging file: %w", err)
|
||||
@@ -166,14 +178,52 @@ func (extractor Extractor) extract(
|
||||
}
|
||||
|
||||
result.Bytes = written
|
||||
result.EntrypointPath = filepath.Join(
|
||||
destination,
|
||||
filepath.FromSlash(normalizedEntrypoint),
|
||||
)
|
||||
result.EntrypointPath = entrypointPath
|
||||
complete = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func planOutputPaths(
|
||||
destination string,
|
||||
entrypoint string,
|
||||
plan []plannedEntry,
|
||||
) (string, string, error) {
|
||||
if destination == "" {
|
||||
return "", "", fmt.Errorf("%w: staging destination is empty", ErrPathEscape)
|
||||
}
|
||||
destinationRoot, err := filepath.Abs(destination)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("%w: resolve staging destination: %v", ErrPathEscape, err)
|
||||
}
|
||||
destinationRoot = filepath.Clean(destinationRoot)
|
||||
for index := range plan {
|
||||
if plan[index].outputPath == "" {
|
||||
plan[index].targetPath = destinationRoot
|
||||
continue
|
||||
}
|
||||
target, err := safepath.JoinUnder(destinationRoot, plan[index].outputPath)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf(
|
||||
"%w: output %q: %v",
|
||||
ErrPathEscape,
|
||||
plan[index].outputPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
plan[index].targetPath = target
|
||||
}
|
||||
entrypointPath, err := safepath.JoinUnder(destinationRoot, entrypoint)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf(
|
||||
"%w: entrypoint %q: %v",
|
||||
ErrEntrypointInvalid,
|
||||
entrypoint,
|
||||
err,
|
||||
)
|
||||
}
|
||||
return destinationRoot, entrypointPath, nil
|
||||
}
|
||||
|
||||
func (extractor Extractor) preflight(
|
||||
archive *zip.Reader,
|
||||
entrypoint string,
|
||||
@@ -200,7 +250,7 @@ func (extractor Extractor) preflight(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folded := strings.ToLower(normalized)
|
||||
folded := safepath.CollisionKey(normalized)
|
||||
if previous, exists := seenPaths[folded]; exists {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: %q conflicts with %q",
|
||||
@@ -305,47 +355,22 @@ func validateArchiveEntry(file *zip.File) (string, bool, error) {
|
||||
}
|
||||
|
||||
func normalizeArchivePath(name string) (string, bool, error) {
|
||||
if name == "" || !utf8.ValidString(name) || strings.ContainsRune(name, '\x00') {
|
||||
return "", false, fmt.Errorf("%w: invalid entry name", ErrPathEscape)
|
||||
}
|
||||
if strings.Contains(name, `\`) || strings.Contains(name, ":") {
|
||||
return "", false, fmt.Errorf("%w: %q", ErrPathEscape, name)
|
||||
}
|
||||
directory := strings.HasSuffix(name, "/")
|
||||
trimmed := strings.TrimSuffix(name, "/")
|
||||
if trimmed == "" || path.IsAbs(trimmed) || strings.HasPrefix(trimmed, "/") {
|
||||
return "", false, fmt.Errorf("%w: %q", ErrPathEscape, name)
|
||||
if err := safepath.ValidateRelative(trimmed); err != nil {
|
||||
return "", false, fmt.Errorf("%w: %q: %v", ErrPathEscape, name, err)
|
||||
}
|
||||
cleaned := path.Clean(trimmed)
|
||||
if cleaned != trimmed ||
|
||||
cleaned == "." ||
|
||||
cleaned == ".." ||
|
||||
strings.HasPrefix(cleaned, "../") {
|
||||
return "", false, fmt.Errorf("%w: %q", ErrPathEscape, name)
|
||||
}
|
||||
return cleaned, directory, nil
|
||||
return trimmed, directory, nil
|
||||
}
|
||||
|
||||
func normalizeEntrypoint(entrypoint string) (string, error) {
|
||||
if entrypoint == "" ||
|
||||
!utf8.ValidString(entrypoint) ||
|
||||
strings.ContainsRune(entrypoint, '\x00') ||
|
||||
strings.Contains(entrypoint, `\`) ||
|
||||
strings.Contains(entrypoint, ":") ||
|
||||
strings.HasSuffix(entrypoint, "/") ||
|
||||
path.IsAbs(entrypoint) {
|
||||
if err := safepath.ValidateRelative(entrypoint); err != nil {
|
||||
return "", fmt.Errorf("%w: %q: %v", ErrEntrypointInvalid, entrypoint, err)
|
||||
}
|
||||
if entrypoint == "payload" || strings.HasPrefix(entrypoint, "payload/") {
|
||||
return "", fmt.Errorf("%w: %q", ErrEntrypointInvalid, entrypoint)
|
||||
}
|
||||
cleaned := path.Clean(entrypoint)
|
||||
if cleaned != entrypoint ||
|
||||
cleaned == "." ||
|
||||
cleaned == ".." ||
|
||||
strings.HasPrefix(cleaned, "../") ||
|
||||
cleaned == "payload" ||
|
||||
strings.HasPrefix(cleaned, "payload/") {
|
||||
return "", fmt.Errorf("%w: %q", ErrEntrypointInvalid, entrypoint)
|
||||
}
|
||||
return cleaned, nil
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
func exceedsCompressionRatio(uncompressed, compressed uint64, maximum float64) bool {
|
||||
|
||||
Reference in New Issue
Block a user