From d7860ddf245ddb495c018ef23e6796f463e2a0d3 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 22 Jun 2026 22:52:01 +0800 Subject: [PATCH] s3api: reject malformed Range offsets (#10034) --- weed/s3api/s3api_object_handlers.go | 16 +++++++--- weed/s3api/s3api_object_range_test.go | 42 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 weed/s3api/s3api_object_range_test.go diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 58830b0bc..37eb4ab6c 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -190,14 +190,22 @@ func (s3a *S3ApiServer) parseAndValidateRange(w http.ResponseWriter, r *http.Req endOffset = totalSize - 1 if parts[0] != "" { - if parsed, err := strconv.ParseInt(parts[0], 10, 64); err == nil { - startOffset = parsed + parsed, parseErr := strconv.ParseInt(parts[0], 10, 64) + if parseErr != nil { + w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", totalSize)) + s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) + return 0, 0, false, newStreamErrorWithResponse(fmt.Errorf("invalid range start: %w", parseErr)) } + startOffset = parsed } if parts[1] != "" { - if parsed, err := strconv.ParseInt(parts[1], 10, 64); err == nil { - endOffset = parsed + parsed, parseErr := strconv.ParseInt(parts[1], 10, 64) + if parseErr != nil { + w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", totalSize)) + s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) + return 0, 0, false, newStreamErrorWithResponse(fmt.Errorf("invalid range end: %w", parseErr)) } + endOffset = parsed } // Special case: range requests on empty files should return 416 diff --git a/weed/s3api/s3api_object_range_test.go b/weed/s3api/s3api_object_range_test.go new file mode 100644 index 000000000..ced294148 --- /dev/null +++ b/weed/s3api/s3api_object_range_test.go @@ -0,0 +1,42 @@ +package s3api + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" +) + +func TestParseAndValidateRangeRejectsMalformedNumericOffsets(t *testing.T) { + s3a := &S3ApiServer{} + entry := &filer_pb.Entry{} + + tests := []struct { + name string + rangeHeader string + }{ + {"invalid start", "bytes=abc-5"}, + {"invalid end", "bytes=5-abc"}, + {"invalid open ended start", "bytes=abc-"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil) + req.Header.Set("Range", tt.rangeHeader) + rec := httptest.NewRecorder() + + _, _, _, streamErr := s3a.parseAndValidateRange(rec, req, entry, 100, "bucket", "object") + if streamErr == nil { + t.Fatalf("parseAndValidateRange(%q) error = nil, want invalid range error", tt.rangeHeader) + } + if rec.Code != http.StatusRequestedRangeNotSatisfiable { + t.Fatalf("parseAndValidateRange(%q) status = %d, want %d", tt.rangeHeader, rec.Code, http.StatusRequestedRangeNotSatisfiable) + } + if got := rec.Header().Get("Content-Range"); got != "bytes */100" { + t.Fatalf("parseAndValidateRange(%q) Content-Range = %q, want %q", tt.rangeHeader, got, "bytes */100") + } + }) + } +}