@@ -15,7 +15,6 @@ import (
1515 "runtime"
1616 "strings"
1717 "sync"
18- "time"
1918
2019 "github.com/chainguard-dev/clog"
2120 "github.com/chainguard-dev/malcontent/pkg/file"
@@ -33,6 +32,34 @@ func init() {
3332 zipPool = pool .NewBufferPool (runtime .GOMAXPROCS (0 ) * 2 )
3433}
3534
35+ // ValidateResolvedPath checks that the target path still resides within the extraction directory
36+ // after resolving symlinks in its parent directory.
37+ func ValidateResolvedPath (target , dir , clean string ) error {
38+ resolvedParent , ok := evalSymlinks (filepath .Dir (target ))
39+ if ! ok {
40+ return nil
41+ }
42+ resolvedDir , ok := evalSymlinks (dir )
43+ if ! ok {
44+ return nil
45+ }
46+ resolvedTarget := filepath .Join (resolvedParent , filepath .Base (target ))
47+ if ! IsValidPath (resolvedTarget , resolvedDir ) {
48+ return fmt .Errorf ("path traversal via symlink in parent directory: %s" , clean )
49+ }
50+ return nil
51+ }
52+
53+ // evalSymlinks resolves symlinks in the given path, returning the resolved path
54+ // and true on success, or an empty string and false if resolution fails.
55+ func evalSymlinks (path string ) (string , bool ) {
56+ resolved , err := filepath .EvalSymlinks (path )
57+ if err != nil {
58+ return "" , false
59+ }
60+ return resolved , true
61+ }
62+
3663// isValidPath checks if the target file is within the given directory.
3764func IsValidPath (target , dir string ) bool {
3865 if strings .Contains (target , "\x00 " ) || strings .Contains (dir , "\x00 " ) {
@@ -137,13 +164,15 @@ func extractNestedArchive(ctx context.Context, c malcontent.Config, d string, f
137164 // Some packages may have archives and files with colliding names
138165 // e.g., demo_page.css and demo_page.css.gz
139166 // the former is the uncompressed version of the latter
140- // if we encounter this, replace the name with something that won't collide
167+ // if we encounter this, use os.MkdirTemp to create a unique directory
141168 if _ , err := os .Stat (archivePath ); err == nil {
142169 logger .Debugf ("duplicate file name already exists, modifying directory name for %s" , archivePath )
143- archivePath = fmt .Sprintf ("%s%d" , archivePath , time .Now ().UnixNano ())
144- }
145-
146- if err := os .MkdirAll (archivePath , 0o700 ); err != nil {
170+ var mkErr error
171+ archivePath , mkErr = os .MkdirTemp (filepath .Dir (archivePath ), filepath .Base (archivePath )+ "_*" )
172+ if mkErr != nil {
173+ return fmt .Errorf ("failed to create unique extraction directory: %w" , mkErr )
174+ }
175+ } else if err := os .MkdirAll (archivePath , 0o700 ); err != nil {
147176 return fmt .Errorf ("failed to create extraction directory: %w" , err )
148177 }
149178
@@ -331,9 +360,19 @@ func handleSymlink(dir, linkPath, linkTarget string) error {
331360 return nil
332361 }
333362
363+ parentDir := filepath .Dir (fullPath )
364+ resolvedDir := dir
365+ if rp , err := filepath .EvalSymlinks (parentDir ); err == nil {
366+ parentDir = rp
367+ if rd , err := filepath .EvalSymlinks (dir ); err == nil {
368+ resolvedDir = rd
369+ }
370+ }
371+
334372 // Validate relative symlink target resolves within extraction directory
335- resolvedTarget := filepath .Clean (filepath .Join (filepath .Dir (fullPath ), linkTarget ))
336- if ! IsValidPath (resolvedTarget , dir ) {
373+ // using the actual (resolved) parent directory
374+ resolvedTarget := filepath .Clean (filepath .Join (parentDir , linkTarget ))
375+ if ! IsValidPath (resolvedTarget , resolvedDir ) {
337376 return fmt .Errorf ("symlink target escapes extraction directory: %s -> %s" , linkPath , linkTarget )
338377 }
339378
@@ -363,8 +402,9 @@ func handleSymlink(dir, linkPath, linkTarget string) error {
363402 return fmt .Errorf ("symlink target mismatch: expected %s, got %s" , linkTarget , actualTarget )
364403 }
365404
366- actualResolved := filepath .Clean (filepath .Join (filepath .Dir (fullPath ), actualTarget ))
367- if ! IsValidPath (actualResolved , dir ) {
405+ // Post-creation validation using the resolved parent directory
406+ actualResolved := filepath .Clean (filepath .Join (parentDir , actualTarget ))
407+ if ! IsValidPath (actualResolved , resolvedDir ) {
368408 os .Remove (fullPath )
369409 return fmt .Errorf ("symlink target escapes extraction directory after creation: %s -> %s" , linkPath , actualTarget )
370410 }
0 commit comments