From 64ab1ac32260238eefca3c61327f5faf1c6e106f Mon Sep 17 00:00:00 2001 From: Rohit Behera <126186063+r0h1tb@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:13:14 +0530 Subject: [PATCH] box: fix truncated files being uploaded successfully when the source ends early The single-shot upload path sent the source straight to Box as a multipart body with no Content-Length, so a source that supplied fewer bytes than its declared size produced a short request that Box accepted and stored, and the upload was reported as a success. Count the bytes actually read and fail the upload if they do not match the declared size. The multipart path already reads each chunk with io.ReadFull and so already fails in this case. --- backend/box/box.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/box/box.go b/backend/box/box.go index 69274e102..35ee52c27 100644 --- a/backend/box/box.go +++ b/backend/box/box.go @@ -45,6 +45,7 @@ import ( "github.com/rclone/rclone/lib/oauthutil" "github.com/rclone/rclone/lib/pacer" "github.com/rclone/rclone/lib/random" + "github.com/rclone/rclone/lib/readers" "github.com/rclone/rclone/lib/rest" "github.com/youmark/pkcs8" ) @@ -1691,7 +1692,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read // upload does a single non-multipart upload // // This is recommended for less than 50 MiB of content -func (o *Object) upload(ctx context.Context, in io.Reader, leaf, directoryID string, modTime time.Time, options ...fs.OpenOption) (err error) { +func (o *Object) upload(ctx context.Context, in io.Reader, leaf, directoryID string, size int64, modTime time.Time, options ...fs.OpenOption) (err error) { upload := api.UploadFile{ Name: o.fs.opt.Enc.FromStandardName(leaf), ContentModifiedAt: api.Time(modTime), @@ -1703,9 +1704,10 @@ func (o *Object) upload(ctx context.Context, in io.Reader, leaf, directoryID str var resp *http.Response var result api.FolderItems + counter := readers.NewCountingReader(in) opts := rest.Opts{ Method: "POST", - Body: in, + Body: counter, MultipartMetadataName: "attributes", MultipartContentName: "contents", MultipartFileName: upload.Name, @@ -1725,6 +1727,11 @@ func (o *Object) upload(ctx context.Context, in io.Reader, leaf, directoryID str if err != nil { return err } + // Check the source supplied the number of bytes it declared + // otherwise a truncated file would be stored as a good upload. + if size >= 0 && int64(counter.BytesRead()) != size { + return fmt.Errorf("expected %d bytes in input, but got %d: %w", size, counter.BytesRead(), io.ErrUnexpectedEOF) + } if result.TotalCount != 1 || len(result.Entries) != 1 { return fmt.Errorf("failed to upload %v - not sure why", o) } @@ -1754,7 +1761,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op // Upload with simple or multipart if size <= int64(o.fs.opt.UploadCutoff) { - err = o.upload(ctx, in, leaf, directoryID, modTime, options...) + err = o.upload(ctx, in, leaf, directoryID, size, modTime, options...) } else { err = o.uploadMultipart(ctx, in, leaf, directoryID, size, modTime, options...) }