Skip to content

fix(b2): validate each range download response in NewRangeReader - #286

Merged
ffumero2003 merged 6 commits into
mainfrom
fix-b2-issue-179-validate-range-response
Nov 4, 2026
Merged

ffumero2003 merged 6 commits into
mainfrom
fix-b2-issue-179-validate-range-response

Conversation

@ffumero2003

Copy link
Copy Markdown
Contributor

Context

NewRangeReader / downloadFile accepted any 2xx download response without checking it satisfied the Range request. A server returning HTTP 200 (whole file) to a nonzero-offset range, a 206 without a matching Content-Range, or a range-ignoring 200 whose body exceeds the requested size, was read as if the bytes started at the requested offset — silently shifting or truncating data, and for a chunked read repeating the object without end.

Root cause

No validation of the range response status / Content-Range at the base HTTP boundary — the only place the status and headers are available.

Changes

  • Add base.ErrInvalidRangeResponse and validateRangeResponse: accept HTTP 200 at offset 0 only when the response encompasses the whole object (declared Content-Length within the requested size, or an unbounded read); otherwise require 206 with a Content-Range whose start equals the requested offset. Error messages name the actual B2 method (by-name or by-id).
  • Cap the body and the advertised Content-Length at the requested size for bounded reads.
  • Correct four existing wire-test fixtures that returned responses B2 never sends (200 at a nonzero offset, or 206 without Content-Range). No SDK assertions changed — only fake response construction.
  • Add range-response tests: bounded oversized-200 rejection, a genuinely unbounded (offset 0, length -1, small ChunkSize) full-object rejection requiring zero returned bytes, by-id method naming, the non-206 status check in isolation, and the Content-Length == size clamp boundary.
  • Record the fix via a changie fragment (no direct CHANGELOG.md edit).

Verification

go test -race ./b2/... ./base green; BASE_REF=gh/main make ci green (build, vet, tidy, verify, lint 0 issues, race, cross-compiles, scripts). Each new test fails without its fix: the by-id method name, the oversized offset-zero 200 (bounded and unbounded), the status check, and the clamp boundary — verified by reverting each guard.

Closes #179

@ffumero2003 ffumero2003 added this to the 0.10.0 milestone Nov 2, 2026
@ffumero2003 ffumero2003 added the bug Something isn't working label Nov 2, 2026
@ffumero2003 ffumero2003 self-assigned this Nov 2, 2026

@david-santacruz-backblaze david-santacruz-backblaze left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mate, a few things to check before merge:

  • Breaking change. base.ErrInvalidRangeResponse is new, and DownloadFile* now errors on a 200 or a missing Content-Range. Mark the changie fragment **Breaking:** and say what callers must do.
  • Content-Range check. #179 wants a matching length and a stop at the total. Only the start is checked, so bytes 2-999999/10 passes.
  • 206 vs 200. A longer 206 body is silently truncated, but a longer 200 errors. Make them consistent.
  • Chunked 200 at offset 0. It's rejected for having no Content-Length. Test it or justify it.
  • Test helpers. Use internal/b2wiretest for the new wire tests, and share one range fake across the four edited fixtures.
  • Unit test. Add a direct base test of validateRangeResponse.

@ffumero2003

Copy link
Copy Markdown
Contributor Author

Fixed in fc8c529. validateRangeResponse now parses and validates the full Content-Range (start == offset, end >= start, total > end), requires the returned span to equal min(size, total-start), and checks Content-Length matches it — so bytes 2-999999/10 is now rejected. HTTP 200 and 206 both reject an over-long declared body consistently, and a chunked response is rejected (its missing length can't be checked before bytes reach the caller) with a comment saying why.

The changelog fragment is now a **Breaking:** Changed entry telling callers to handle ErrInvalidRangeResponse. The new wire tests share one range helper in internal/b2wiretest (deduping the four fixtures), and there's a direct base unit test of validateRangeResponse (good, oversized 206, wrong total, malformed, chunked). go test -race -count=5 -shuffle=on ./base ./b2/..., go vet, gofmt and make ci all clean. Thanks for the thorough review.

Range downloads accepted full-object replies and trusted their content length, allowing a server that ignores Range to return bytes past the requested bound and repeat unbounded reads.

Require a partial response at the requested offset and limit each response to its requested chunk.

Closes #179
Make the affected fake servers follow B2 response semantics: whole-file offset-zero ranges return 200, while partial ranges return 206 with a matching Content-Range.

Add the current-main changelog fragment required for the production validation change.

Closes #179
Report the actual B2 download method in invalid-range errors and accept an offset-zero HTTP 200 only when its declared body fits the requested range. Strengthen bounded and unbounded reader regressions so an oversized full-object response yields no bytes.

Keep the release note in the changie fragment and record that this branch corrects four pre-existing wire fixtures: downloadbyid_extended, progress, sha1, and ssec.

Closes #179
Reject a non-partial response even when its Content-Range otherwise matches the requested range, and preserve an equal Content-Length header without applying the oversized-response clamp.

Closes #179
Validate the end, total, and declared length of each partial response, and reject oversized or chunked replies before exposing bytes. Share one range fixture across wire tests and document the breaking error behavior.

Closes #179
Use the shared range response helper so one-byte chunks carry matching Content-Range and Content-Length headers after strict response validation.

Closes #179
@ffumero2003
ffumero2003 force-pushed the fix-b2-issue-179-validate-range-response branch from fc8c529 to 7add7ab Compare November 4, 2026 16:57
@ffumero2003

ffumero2003 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

CI is green after rebasing on main (7add7ab). The only fake that tripped the stricter validation was TestReaderChunkSizeOneAndDefaults; its handler now uses the shared b2wiretest.RangeDownload helper so one-byte ranges return valid 206 responses. Full suite (with and without -race), gofmt, vet and make ci are clean.

On live coverage (per the review question): the B2-dependent behavior is already exercised live by TestRangeReaderLive (b2/integration_test.go), which runs real NewRangeReader downloads through the new validateRangeResponse — a non-conforming real response would make it fail. The 200/full-download path is covered the same way by the existing download live tests. I'll run TestRangeReaderLive against real B2 and post the result here to confirm the new validation doesn't reject legitimate B2 responses.

@ffumero2003
ffumero2003 merged commit 4e9f705 into main Nov 4, 2026
26 of 27 checks passed
@ffumero2003
ffumero2003 deleted the fix-b2-issue-179-validate-range-response branch November 4, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validate each range response in NewRangeReader so a full-object reply cannot return extra bytes

2 participants