@@ -249,34 +249,28 @@ func isWindowsPath(path string) bool {
249249
250250// sanitizeTarPath validates and sanitizes a tar entry path to prevent path traversal attacks.
251251// Returns the sanitized target path or an error if the path is malicious.
252+ // Uses path package (not filepath) because tar paths and guest paths use forward slashes.
252253func sanitizeTarPath (basePath , entryName string ) (string , error ) {
253- // Clean the entry name
254- clean := filepath .Clean (entryName )
254+ // Clean the entry name using path.Clean (forward slashes for guest/tar paths)
255+ clean := path .Clean (entryName )
255256
256- // Reject absolute paths
257- if filepath . IsAbs (clean ) {
257+ // Reject absolute paths (Linux paths start with /)
258+ if strings . HasPrefix (clean , "/" ) {
258259 return "" , fmt .Errorf ("invalid tar entry: absolute path not allowed: %s" , entryName )
259260 }
260261
261- // Reject paths that start with ..
262+ // Reject paths that start with .. (escaping destination)
262263 if strings .HasPrefix (clean , ".." ) {
263264 return "" , fmt .Errorf ("invalid tar entry: path escapes destination: %s" , entryName )
264265 }
265266
266- // Join with base path
267- targetPath := filepath .Join (basePath , clean )
267+ // Join with base path using path.Join (forward slashes for guest paths)
268+ targetPath := path .Join (basePath , clean )
268269
269270 // Verify the result is under the base path
270- absBase , err := filepath .Abs (basePath )
271- if err != nil {
272- return "" , fmt .Errorf ("resolve base path: %w" , err )
273- }
274- absTarget , err := filepath .Abs (targetPath )
275- if err != nil {
276- return "" , fmt .Errorf ("resolve target path: %w" , err )
277- }
278-
279- if ! strings .HasPrefix (absTarget , absBase + string (filepath .Separator )) && absTarget != absBase {
271+ // path.Clean removes trailing slashes, so compare cleaned versions
272+ cleanBase := path .Clean (basePath )
273+ if ! strings .HasPrefix (targetPath , cleanBase + "/" ) && targetPath != cleanBase {
280274 return "" , fmt .Errorf ("invalid tar entry: path escapes destination: %s" , entryName )
281275 }
282276
@@ -793,6 +787,7 @@ func copyTarFileToInstance(ctx context.Context, baseURL, apiKey, instanceID stri
793787 ws , resp , err := dialer .DialContext (ctx , wsURL , headers )
794788 if err != nil {
795789 if resp != nil {
790+ defer resp .Body .Close ()
796791 body , _ := io .ReadAll (resp .Body )
797792 return fmt .Errorf ("websocket connect failed (HTTP %d): %s" , resp .StatusCode , string (body ))
798793 }
@@ -894,6 +889,7 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
894889
895890 var currentHeader * cpFileHeader
896891 var fileData []byte
892+ var receivedFinal bool
897893
898894 for {
899895 msgType , message , err := ws .ReadMessage ()
@@ -970,6 +966,7 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
970966 var endMarker cpEndMarker
971967 json .Unmarshal (message , & endMarker )
972968 if endMarker .Final {
969+ receivedFinal = true
973970 return nil
974971 }
975972
@@ -984,6 +981,10 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
984981 }
985982 }
986983
984+ // If connection closed without receiving final marker, the transfer was incomplete
985+ if ! receivedFinal {
986+ return fmt .Errorf ("copy stream ended without completion marker" )
987+ }
987988 return nil
988989}
989990
0 commit comments