@@ -186,7 +186,7 @@ func handleCp(ctx context.Context, cmd *cli.Command) error {
186186 if srcPath == "-" {
187187 return copyFromStdinToInstance (ctx , baseURL , apiKey , instanceID , dstPath , archive )
188188 }
189- return copyToInstance (ctx , baseURL , apiKey , instanceID , srcPath , dstPath , quiet , archive )
189+ return copyToInstance (ctx , baseURL , apiKey , instanceID , srcPath , dstPath , quiet , archive , followLinks )
190190 }
191191}
192192
@@ -246,6 +246,42 @@ func isWindowsPath(path string) bool {
246246 return false
247247}
248248
249+ // sanitizeTarPath validates and sanitizes a tar entry path to prevent path traversal attacks.
250+ // Returns the sanitized target path or an error if the path is malicious.
251+ func sanitizeTarPath (basePath , entryName string ) (string , error ) {
252+ // Clean the entry name
253+ clean := filepath .Clean (entryName )
254+
255+ // Reject absolute paths
256+ if filepath .IsAbs (clean ) {
257+ return "" , fmt .Errorf ("invalid tar entry: absolute path not allowed: %s" , entryName )
258+ }
259+
260+ // Reject paths that start with ..
261+ if strings .HasPrefix (clean , ".." ) {
262+ return "" , fmt .Errorf ("invalid tar entry: path escapes destination: %s" , entryName )
263+ }
264+
265+ // Join with base path
266+ targetPath := filepath .Join (basePath , clean )
267+
268+ // Verify the result is under the base path
269+ absBase , err := filepath .Abs (basePath )
270+ if err != nil {
271+ return "" , fmt .Errorf ("resolve base path: %w" , err )
272+ }
273+ absTarget , err := filepath .Abs (targetPath )
274+ if err != nil {
275+ return "" , fmt .Errorf ("resolve target path: %w" , err )
276+ }
277+
278+ if ! strings .HasPrefix (absTarget , absBase + string (filepath .Separator )) && absTarget != absBase {
279+ return "" , fmt .Errorf ("invalid tar entry: path escapes destination: %s" , entryName )
280+ }
281+
282+ return targetPath , nil
283+ }
284+
249285// statGuestPath queries the guest for information about a path
250286func statGuestPath (ctx context.Context , baseURL , apiKey , instanceID , guestPath string , followLinks bool ) (* cpStatResponse , error ) {
251287 wsURL , err := buildCpWsURL (baseURL , instanceID )
@@ -389,7 +425,7 @@ func buildCpWsURL(baseURL, instanceID string) (string, error) {
389425}
390426
391427// copyToInstance copies a local file/directory to the instance
392- func copyToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive bool ) error {
428+ func copyToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive , followLinks bool ) error {
393429 // Check for /. suffix (copy contents only)
394430 copyContentsOnly := strings .HasSuffix (srcPath , string (filepath .Separator )+ "." ) || strings .HasSuffix (srcPath , "/." )
395431 originalSrcPath := srcPath
@@ -413,15 +449,15 @@ func copyToInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPath, d
413449 if srcInfo .IsDir () {
414450 if copyContentsOnly {
415451 // Copy contents of srcPath into resolvedDst
416- return copyDirContentsToInstance (ctx , baseURL , apiKey , instanceID , srcPath , resolvedDst , quiet , archive )
452+ return copyDirContentsToInstance (ctx , baseURL , apiKey , instanceID , srcPath , resolvedDst , quiet , archive , followLinks )
417453 }
418- return copyDirToInstance (ctx , baseURL , apiKey , instanceID , srcPath , resolvedDst , quiet , archive )
454+ return copyDirToInstance (ctx , baseURL , apiKey , instanceID , srcPath , resolvedDst , quiet , archive , followLinks )
419455 }
420- return copyFileToInstance (ctx , baseURL , apiKey , instanceID , srcPath , resolvedDst , srcInfo .Mode ().Perm (), quiet , archive )
456+ return copyFileToInstance (ctx , baseURL , apiKey , instanceID , srcPath , resolvedDst , srcInfo .Mode ().Perm (), quiet , archive , followLinks )
421457}
422458
423459// copyFileToInstance copies a single file to the instance using the SDK
424- func copyFileToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , mode fs.FileMode , quiet , archive bool ) error {
460+ func copyFileToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , mode fs.FileMode , quiet , archive , followLinks bool ) error {
425461 srcInfo , err := os .Stat (srcPath )
426462 if err != nil {
427463 return fmt .Errorf ("stat source: %w" , err )
@@ -442,12 +478,13 @@ func copyFileToInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPat
442478 }
443479
444480 err = lib .CpToInstance (ctx , cfg , lib.CpToInstanceOptions {
445- InstanceID : instanceID ,
446- SrcPath : srcPath ,
447- DstPath : dstPath ,
448- Mode : mode ,
449- Archive : archive ,
450- Callbacks : callbacks ,
481+ InstanceID : instanceID ,
482+ SrcPath : srcPath ,
483+ DstPath : dstPath ,
484+ Mode : mode ,
485+ Archive : archive ,
486+ FollowLinks : followLinks ,
487+ Callbacks : callbacks ,
451488 })
452489 if err != nil {
453490 return err
@@ -457,7 +494,7 @@ func copyFileToInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPat
457494}
458495
459496// copyDirToInstance copies a directory recursively to the instance using the SDK
460- func copyDirToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive bool ) error {
497+ func copyDirToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive , followLinks bool ) error {
461498 cfg := lib.CpConfig {
462499 BaseURL : baseURL ,
463500 APIKey : apiKey ,
@@ -474,11 +511,12 @@ func copyDirToInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPath
474511
475512 // First create the destination directory
476513 err := lib .CpToInstance (ctx , cfg , lib.CpToInstanceOptions {
477- InstanceID : instanceID ,
478- SrcPath : srcPath ,
479- DstPath : dstPath ,
480- Archive : archive ,
481- Callbacks : callbacks ,
514+ InstanceID : instanceID ,
515+ SrcPath : srcPath ,
516+ DstPath : dstPath ,
517+ Archive : archive ,
518+ FollowLinks : followLinks ,
519+ Callbacks : callbacks ,
482520 })
483521 if err != nil {
484522 return err
@@ -489,7 +527,7 @@ func copyDirToInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPath
489527
490528// copyDirContentsToInstance copies only the contents of a directory (not the directory itself)
491529// This implements the /. suffix behavior from docker cp
492- func copyDirContentsToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive bool ) error {
530+ func copyDirContentsToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive , followLinks bool ) error {
493531 entries , err := os .ReadDir (srcPath )
494532 if err != nil {
495533 return fmt .Errorf ("read directory: %w" , err )
@@ -519,12 +557,13 @@ func copyDirContentsToInstance(ctx context.Context, baseURL, apiKey, instanceID,
519557 }
520558
521559 if err := lib .CpToInstance (ctx , cfg , lib.CpToInstanceOptions {
522- InstanceID : instanceID ,
523- SrcPath : srcEntryPath ,
524- DstPath : dstEntryPath ,
525- Mode : info .Mode ().Perm (),
526- Archive : archive ,
527- Callbacks : callbacks ,
560+ InstanceID : instanceID ,
561+ SrcPath : srcEntryPath ,
562+ DstPath : dstEntryPath ,
563+ Mode : info .Mode ().Perm (),
564+ Archive : archive ,
565+ FollowLinks : followLinks ,
566+ Callbacks : callbacks ,
528567 }); err != nil {
529568 return err
530569 }
@@ -695,7 +734,11 @@ func copyFromStdinToInstance(ctx context.Context, baseURL, apiKey, instanceID, d
695734 return fmt .Errorf ("read tar header: %w" , err )
696735 }
697736
698- targetPath := filepath .Join (dstPath , header .Name )
737+ // Sanitize tar entry path to prevent path traversal attacks
738+ targetPath , err := sanitizeTarPath (dstPath , header .Name )
739+ if err != nil {
740+ return err
741+ }
699742
700743 // Extract uid/gid from tar header if archive mode
701744 var uid , gid uint32
@@ -860,8 +903,8 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
860903
861904 switch msgTypeStr {
862905 case "header" :
863- // Flush previous file if any
864- if currentHeader != nil && ! currentHeader .IsDir {
906+ // Flush previous file if any (but not symlinks - they have no data)
907+ if currentHeader != nil && ! currentHeader .IsDir && ! currentHeader . IsSymlink {
865908 if err := writeTarEntry (tw , currentHeader , fileData ); err != nil {
866909 return err
867910 }
0 commit comments