diff options
Diffstat (limited to 'hotline')
| -rw-r--r-- | hotline/file_transfer.go | 21 | ||||
| -rw-r--r-- | hotline/file_transfer_test.go | 62 |
2 files changed, 75 insertions, 8 deletions
diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index 86e10eb..8670439 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -209,7 +209,9 @@ func (fu *folderUpload) FormattedPath() string { } } - return path.Join(pathSegments...) + // Anchor at "/" so any ".." segments are collapsed and cannot escape the upload + // root (mirrors ReadPath). Strip the leading separator to keep the path relative. + return strings.TrimPrefix(path.Join("/", path.Join(pathSegments...)), "/") } type FileHeader struct { @@ -561,9 +563,12 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT return err } + // Resolve the item path once. FormattedPath is sanitized to stay within fullPath. + itemPath := path.Join(fullPath, fu.FormattedPath()) + if fu.IsFolder == [2]byte{0, 1} { - if _, err := os.Stat(path.Join(fullPath, fu.FormattedPath())); os.IsNotExist(err) { - if err := os.Mkdir(path.Join(fullPath, fu.FormattedPath()), 0777); err != nil { + if _, err := os.Stat(itemPath); os.IsNotExist(err) { + if err := os.Mkdir(itemPath, 0777); err != nil { return err } } @@ -576,7 +581,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT nextAction := DlFldrActionSendFile // Check if we have the full file already. If so, send dlFldrAction_NextFile to client to skip. - _, err := os.Stat(path.Join(fullPath, fu.FormattedPath())) + _, err := os.Stat(itemPath) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -585,7 +590,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT } // Check if we have a partial file already. If so, send dlFldrAction_ResumeFile to client to resume upload. - incompleteFile, err := os.Stat(path.Join(fullPath, fu.FormattedPath()+IncompleteFileSuffix)) + incompleteFile, err := os.Stat(itemPath + IncompleteFileSuffix) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -604,7 +609,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT offset := make([]byte, 4) binary.BigEndian.PutUint32(offset, uint32(incompleteFile.Size())) - file, err := os.OpenFile(fullPath+"/"+fu.FormattedPath()+IncompleteFileSuffix, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile(itemPath+IncompleteFileSuffix, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } @@ -628,7 +633,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT rLogger.Error("Error receiving file", "err", err) } - err = os.Rename(fullPath+"/"+fu.FormattedPath()+".incomplete", fullPath+"/"+fu.FormattedPath()) + err = os.Rename(itemPath+IncompleteFileSuffix, itemPath) if err != nil { return err } @@ -638,7 +643,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT return err } - filePath := path.Join(fullPath, fu.FormattedPath()) + filePath := itemPath hlFile, err := NewFile(fileStore, filePath, 0) if err != nil { diff --git a/hotline/file_transfer_test.go b/hotline/file_transfer_test.go index a5c4afe..d427352 100644 --- a/hotline/file_transfer_test.go +++ b/hotline/file_transfer_test.go @@ -255,6 +255,68 @@ func Test_folderUpload_FormattedPath(t *testing.T) { }, want: "test@$%&", }, + { + // Traversal attempt: leading ".." segments must be collapsed and cannot escape the upload root. + name: "traversal with parent dir segments", + pathItemCount: [2]byte{0x00, 0x04}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x02, // segment length + 0x2e, 0x2e, // ".." + 0x00, 0x00, // path separator + 0x02, // segment length + 0x2e, 0x2e, // ".." + 0x00, 0x00, // path separator + 0x03, // segment length + 0x65, 0x74, 0x63, // "etc" + 0x00, 0x00, // path separator + 0x06, // segment length + 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, // "passwd" + }, + want: "etc/passwd", + }, + { + // Interior ".." segments resolve away without escaping. + name: "traversal with interior parent dir segments", + pathItemCount: [2]byte{0x00, 0x04}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x03, // segment length + 0x66, 0x6f, 0x6f, // "foo" + 0x00, 0x00, // path separator + 0x02, // segment length + 0x2e, 0x2e, // ".." + 0x00, 0x00, // path separator + 0x02, // segment length + 0x2e, 0x2e, // ".." + 0x00, 0x00, // path separator + 0x03, // segment length + 0x62, 0x61, 0x72, // "bar" + }, + want: "bar", + }, + { + // A lone ".." segment collapses to the upload root (empty relative path). + name: "single parent dir segment", + pathItemCount: [2]byte{0x00, 0x01}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x02, // segment length + 0x2e, 0x2e, // ".." + }, + want: "", + }, + { + // A single segment whose raw bytes embed separators and "..". + name: "segment containing embedded separators", + pathItemCount: [2]byte{0x00, 0x01}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x09, // segment length + 0x2e, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x65, 0x74, 0x63, // "../../etc" (9 bytes) + }, + want: "etc", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { |