Skip to content

Commit ac351be

Browse files
fix: safely handle symlinks during unarchive
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 0d8bea3 commit ac351be

1 file changed

Lines changed: 56 additions & 6 deletions

File tree

files/files.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,35 @@ import (
2525
var blacklistedPathSymbols = "${}[]?*:<>|"
2626
var blockedPrefixes = []string{"/run/", "/proc/", "/etc/", "/var/", "/tmp/", "/dev/"}
2727

28+
// safeJoinEvalSymlinks joins base and path, resolves symlinks, and ensures result is inside base
29+
func safeJoinEvalSymlinks(base, p string) (string, error) {
30+
joined := filepath.Join(base, p)
31+
resolved, err := filepath.EvalSymlinks(joined)
32+
if err != nil && !os.IsNotExist(err) {
33+
return "", err
34+
}
35+
// If file does not exist yet (may be new), fallback to cleaned join
36+
if err != nil && os.IsNotExist(err) {
37+
resolved = filepath.Clean(joined)
38+
}
39+
absBase, err := filepath.Abs(base)
40+
if err != nil {
41+
return "", err
42+
}
43+
absResolved, err := filepath.Abs(resolved)
44+
if err != nil {
45+
return "", err
46+
}
47+
rel, err := filepath.Rel(absBase, absResolved)
48+
if err != nil {
49+
return "", err
50+
}
51+
if strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
52+
return "", fmt.Errorf("illegal file path: %s escapes base directory %s", absResolved, absBase)
53+
}
54+
return absResolved, nil
55+
}
56+
2857
func isASCII(s string) bool {
2958
for i := 0; i < len(s); i++ {
3059

@@ -311,13 +340,21 @@ func UntarWithFilter(tarball, target string, filter FileFilter) error {
311340
if err := ValidatePath(header.Name); err != nil {
312341
return err
313342
}
314-
path := filepath.Join(target, header.Name)
343+
extractPath, err := safeJoinEvalSymlinks(target, header.Name)
344+
if err != nil {
345+
return fmt.Errorf("invalid extracted path: %w", err)
346+
}
347+
path := extractPath
315348
if filter != nil {
316-
path = filter(info)
317-
if path == "" {
349+
fp := filter(info)
350+
if fp == "" {
318351
continue
319352
}
320-
path = filepath.Join(target, path)
353+
newPath, err := safeJoinEvalSymlinks(target, fp)
354+
if err != nil {
355+
return fmt.Errorf("invalid filtered path: %w", err)
356+
}
357+
path = newPath
321358
}
322359
if info.IsDir() {
323360
if err = os.MkdirAll(path, info.Mode()); err != nil {
@@ -351,8 +388,21 @@ func UntarWithFilter(tarball, target string, filter FileFilter) error {
351388
return fmt.Errorf("failed to remove symlink %s: %w", path, err)
352389
}
353390

354-
if err := os.Symlink(header.Linkname, path); err != nil {
355-
return fmt.Errorf("failed to create symlink %s -> %s: %w", path, header.Linkname, err)
391+
// Validate the symlink target stays within extraction dir
392+
linkTarget := header.Linkname
393+
// Only check relative paths; absolute always forbidden
394+
if filepath.IsAbs(linkTarget) {
395+
return fmt.Errorf("symlink target %s is absolute and not allowed", linkTarget)
396+
evalTarget, err := safeJoinEvalSymlinks(filepath.Dir(path), linkTarget)
397+
if err != nil {
398+
return fmt.Errorf("invalid symlink target from %s to %s: %w", path, linkTarget, err)
399+
}
400+
if !strings.HasPrefix(evalTarget, filepath.Clean(target)+string(os.PathSeparator)) && filepath.Clean(evalTarget) != filepath.Clean(target) {
401+
return fmt.Errorf("symlink %s target %s would escape extraction root", path, linkTarget)
402+
}
403+
if err := os.Symlink(linkTarget, path); err != nil {
404+
return fmt.Errorf("failed to create symlink %s -> %s: %w", path, linkTarget, err)
405+
}
356406
}
357407

358408
case tar.TypeDir:

0 commit comments

Comments
 (0)