@@ -189,24 +189,48 @@ func (t *rangeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
189189 return resp , err
190190 }
191191
192- // If we requested a Range, record success only if the server accepted the range request
193- // Servers should return 206 (Partial Content) for successful range requests,
194- // but some may return 200 with the partial content, so we record success for both
195- if requestedOffset > 0 {
196- if resp .StatusCode == http .StatusPartialContent || resp .StatusCode == http .StatusOK {
197- // Record in RangeSuccess tracker so WriteBlob can check it
192+ // If we requested a Range, record success only when the server honoured it
193+ // with 206 Partial Content and a matching Content-Range start offset. A 200
194+ // response means the server ignored the Range header and is sending the full
195+ // file from byte 0; appending that stream to the existing partial file would
196+ // produce a corrupt blob. We also validate the Content-Range start offset to
197+ // guard against a misbehaving server that returns 206 with a different range.
198+ if requestedOffset > 0 && resp .StatusCode == http .StatusPartialContent {
199+ if rangeStartMatchesOffset (resp .Header .Get ("Content-Range" ), requestedOffset ) {
198200 if rs := GetRangeSuccess (req .Context ()); rs != nil {
199201 rs .Add (digest , requestedOffset )
200202 }
201203 }
202- // If range request was not successful (e.g., 416 Range Not Satisfiable),
203- // don't record in RangeSuccess, which will cause WriteBlob to start fresh
204- // (no explicit action needed in the else case)
205204 }
206205
207206 return resp , nil
208207}
209208
209+ // rangeStartMatchesOffset parses the Content-Range response header and reports
210+ // whether its start byte equals the given offset. The format is defined by
211+ // RFC 9110: "bytes START-END/TOTAL" (TOTAL may be "*"). We fail closed: if the
212+ // header is absent or cannot be parsed we return false so that the caller does
213+ // not treat an ambiguous response as a successful range request.
214+ func rangeStartMatchesOffset (contentRange string , offset int64 ) bool {
215+ if contentRange == "" {
216+ return false
217+ }
218+ // Trim the unit prefix "bytes " and split on "-"
219+ after , ok := strings .CutPrefix (contentRange , "bytes " )
220+ if ! ok {
221+ return false
222+ }
223+ dashIdx := strings .Index (after , "-" )
224+ if dashIdx < 0 {
225+ return false
226+ }
227+ var start int64
228+ if _ , err := fmt .Sscanf (after [:dashIdx ], "%d" , & start ); err != nil {
229+ return false
230+ }
231+ return start == offset
232+ }
233+
210234// extractDigestAndOffset extracts the blob digest from the request URL and returns
211235// the corresponding resume offset if one exists.
212236func (t * rangeTransport ) extractDigestAndOffset (req * http.Request , offsets map [string ]int64 ) (string , int64 ) {
0 commit comments