@@ -25,6 +25,35 @@ import (
2525var blacklistedPathSymbols = "${}[]?*:<>|"
2626var 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+
2857func 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