@@ -77,6 +77,14 @@ type Downloader struct {
7777 // operation requests made by the downloader.
7878 ClientOptions []func (* s3.Options )
7979
80+ // By default, the downloader verifies that individual part ranges align
81+ // based on the configured part size.
82+ //
83+ // You can disable that with this flag, however, Amazon S3 recommends
84+ // against doing so because it damages the durability posture of object
85+ // downloads.
86+ DisableValidateParts bool
87+
8088 // Defines the buffer strategy used when downloading a part.
8189 //
8290 // If a WriterReadFromProvider is given the Download manager
@@ -404,6 +412,15 @@ func (d *downloader) tryDownloadChunk(params *s3.GetObjectInput, w io.Writer) (i
404412 if err != nil {
405413 return 0 , err
406414 }
415+
416+ if ! d .cfg .DisableValidateParts && params .Range != nil && resp .ContentRange != nil {
417+ expectStart , expectEnd := parseContentRange (* params .Range )
418+ actualStart , actualEnd := parseContentRange (* resp .ContentRange )
419+ if isRangeMismatch (expectStart , expectEnd , actualStart , actualEnd ) {
420+ return 0 , fmt .Errorf ("invalid content range: expect %d-%d, got %d-%d" , expectStart , expectEnd , actualStart , actualEnd )
421+ }
422+ }
423+
407424 d .setTotalBytes (resp ) // Set total if not yet set.
408425 d .once .Do (func () {
409426 d .etag = aws .ToString (resp .ETag )
@@ -422,6 +439,39 @@ func (d *downloader) tryDownloadChunk(params *s3.GetObjectInput, w io.Writer) (i
422439 return n , nil
423440}
424441
442+ func parseContentRange (v string ) (int , int ) {
443+ parts := strings .Split (v , "/" ) // chop the total off, if it's there
444+
445+ // we send "bytes=" but S3 appears to return "bytes ", handle both
446+ trimmed := strings .TrimPrefix (parts [0 ], "bytes " )
447+ trimmed = strings .TrimPrefix (trimmed , "bytes=" )
448+
449+ parts = strings .Split (trimmed , "-" )
450+ if len (parts ) != 2 {
451+ return - 1 , - 1
452+ }
453+
454+ start , err := strconv .Atoi (parts [0 ])
455+ if err != nil {
456+ return - 1 , - 1
457+ }
458+
459+ end , err := strconv .Atoi (parts [1 ])
460+ if err != nil {
461+ return - 1 , - 1
462+ }
463+
464+ return start , end
465+ }
466+
467+ func isRangeMismatch (expectStart , expectEnd , actualStart , actualEnd int ) bool {
468+ if expectStart == - 1 || expectEnd == - 1 || actualStart == - 1 || actualEnd == - 1 {
469+ return false // we don't know, one of the ranges was missing or unparseable
470+ }
471+
472+ return expectStart != actualStart && expectEnd != actualEnd
473+ }
474+
425475// getTotalBytes is a thread-safe getter for retrieving the total byte status.
426476func (d * downloader ) getTotalBytes () int64 {
427477 d .m .Lock ()
0 commit comments