fix(volume): reject a negative suffix length in Range headers (#12269)

* fix(volume): reject a negative suffix length in Range headers

parseRange accepted "bytes=--5" as a suffix range of -5 bytes and
returned {start: size+5, length: -5}. The volume server then answered
206 with an empty body and Content-Range "bytes 15-9/10", and the
filer's inline-content reader sliced entry.Content[15:10] and panicked.

Restore the check Go's net/http parseRange has: a suffix-length must be
a non-negative integer (RFC 7233 2.1), so a leading '-' or a negative
value is an invalid range. Apply the same check to the Rust volume
server's parse_range_header.

* test: negative-suffix manifest read is now rejected with 416

The parser change makes a negative suffix an invalid-range error, so
the duplicate-range case in this test can no longer reach the "empty
200" path. Assert the new rejection and that nothing is read.

* volume: treat a zero-length suffix range as unsatisfiable

"bytes=-0" resolved to {start: size, length: 0}, and the range handler
then answered 206 with "Content-Range: bytes 10-9/10", a value clients
must not recombine (and a first-byte-pos RFC 9110 cannot express).
Classify the zero-length suffix with the existing no-overlap case —
its resolved first-byte-pos sits at the content size — so the request
gets a 416 "bytes */size" unless another range in the set overlaps.
Applies to the Go and Rust volume servers alike.

---------

Co-authored-by: chrislusf <chrislusf@gmail.com>
Co-authored-by: user.email <chrislusf@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-31 05:09:14 -07:00
1 parent 97e1758bcc
commit 6cf6ce81d7
4 files changed
+67 -9

No files matched your search

+3
View File
@@ -325,6 +325,9 @@ func TestProcessRangeRequestRanges(t *testing.T) {
{"bytes=10-", http.StatusRequestedRangeNotSatisfiable, "bytes */10", ""},
{"bytes=100-", http.StatusRequestedRangeNotSatisfiable, "bytes */10", ""},
{"bytes=10-,0-1", http.StatusPartialContent, "bytes 0-1/10", "01"},
{"bytes=--5", http.StatusRequestedRangeNotSatisfiable, "", ""},
{"bytes=-0", http.StatusRequestedRangeNotSatisfiable, "bytes */10", ""},
{"bytes=-0,0-1", http.StatusPartialContent, "bytes 0-1/10", "01"},
}
for _, tt := range tests {
w, err := serve(tt.rangeHeader)
+15 -2
View File
@@ -55,15 +55,28 @@ func parseRange(s string, size int64) ([]httpRange, error) {
var r httpRange
if start == "" {
// If no start is specified, end specifies the
// range start relative to the end of the file.
// range start relative to the end of the file,
// and we are dealing with <suffix-length>
// which has to be a non-negative integer as per
// RFC 7233 Section 2.1 "Byte-Ranges".
if end == "" || end[0] == '-' {
return nil, errors.New("invalid range")
}
i, err := strconv.ParseInt(end, 10, 64)
if err != nil {
if i < 0 || err != nil {
return nil, errors.New("invalid range")
}
if i > size {
i = size
}
r.start = size - i
if r.start >= size {
// a zero-length suffix starts at the content size, so it
// cannot overlap (RFC 9110 14.1.2: a suffix-range needs a
// non-zero suffix-length to be satisfiable)
noOverlap = true
continue
}
r.length = size - r.start
} else {
i, err := strconv.ParseInt(start, 10, 64)
@@ -19,7 +19,12 @@ func TestParseRange(t *testing.T) {
{name: "start with explicit end within bounds", rangeSpec: "bytes=2-5", wantStart: 2, wantLen: 4},
{name: "end clamped to the last byte", rangeSpec: "bytes=2-100", wantStart: 2, wantLen: 8},
{name: "suffix range within bounds", rangeSpec: "bytes=-3", wantStart: 7, wantLen: 3},
{name: "zero-length suffix range is not satisfiable", rangeSpec: "bytes=-0", wantErr: true},
{name: "zero-length suffix beside a satisfiable range is skipped", rangeSpec: "bytes=-0,0-1", wantStart: 0, wantLen: 2},
{name: "suffix range larger than size is clamped, not rejected", rangeSpec: "bytes=-100", wantStart: 0, wantLen: 10},
{name: "negative suffix length is invalid", rangeSpec: "bytes=--5", wantErr: true},
{name: "negative zero suffix length is invalid", rangeSpec: "bytes=--0", wantErr: true},
{name: "empty suffix length is invalid", rangeSpec: "bytes=-", wantErr: true},
}
for _, tt := range tests {
@@ -52,4 +57,10 @@ func TestParseRangeOnEmptyFile(t *testing.T) {
if _, err := parseRange("bytes=0-", 0); err == nil {
t.Fatal("parseRange(\"bytes=0-\", 0) = nil error, want an error")
}
if _, err := parseRange("bytes=-0", 0); err == nil {
t.Fatal("parseRange(\"bytes=-0\", 0) = nil error, want an error")
}
if _, err := parseRange("bytes=-5", 0); err == nil {
t.Fatal("parseRange(\"bytes=-5\", 0) = nil error, want an error")
}
}