doc/godebug.md | 18 +++++++++++++++++- src/internal/godebugs/table.go | 1 + src/net/http/fs.go | 28 ++++++++++++++++++++++++++++ src/net/http/range_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/runtime/metrics/doc.go | 5 +++++ diff --git a/doc/godebug.md b/doc/godebug.md index df4a1155b3137b10cee4775e082f58acc3610b08..f6198735eac49219e8eecf30de716309076772da 100644 --- a/doc/godebug.md +++ b/doc/godebug.md @@ -151,6 +151,22 @@ for example, see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables) and the [go command documentation](/cmd/go#hdr-Build_and_test_caching). +### Go 1.28 + +Go 1.28 changed +[`net/http.FileServer`](/pkg/net/http#FileServer), +[`net/http.FileServerFS`](/pkg/net/http#FileServerFS), +[`net/http.ServeContent`](/pkg/net/http#ServeContent), +[`net/http.ServeFile`](/pkg/net/http#ServeFile), and +[`net/http.ServeFileFS`](/pkg/net/http#ServeFileFS) to +limit the maximum number of ranges in a Range header. +When the number of ranges in a header exceeds the new +`httpservecontentmaxranges` setting, the header is ignored. +The default value is `httpservecontentmaxranges=200`. +Setting `httpservecontentmaxranges=0` disables the limit. +To avoid denial of service attacks, this setting and default +was backported to Go 1.27.2 and Go 1.26.9. + ### Go 1.26 Go 1.26.6 added a `fips140ems` setting that when set to `0` disables the @@ -164,7 +180,7 @@ escaped. Setting `htmlmetacontentescape=0` disables this behavior. Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number of cookies that net/http will accept when parsing HTTP headers. If the number of -cookie in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing +cookies in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing will fail early. The default value is `httpcookiemaxnum=3000`. Setting `httpcookiemaxnum=0` will allow the cookie parsing to accept an indefinite number of cookies. To avoid denial of service attacks, this setting and default diff --git a/src/internal/godebugs/table.go b/src/internal/godebugs/table.go index 4b4c6c1ebeb137e90bf5067968cfdadcbd1322a4..f0a1587abe7065ad7d370330fb5c32616ba1de0d 100644 --- a/src/internal/godebugs/table.go +++ b/src/internal/godebugs/table.go @@ -49,6 +49,7 @@ {Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"}, {Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"}, {Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"}, {Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "1"}, + {Name: "httpservecontentmaxranges", Package: "net/http", Changed: 26, Old: "1"}, {Name: "installgoroot", Package: "go/build"}, {Name: "jstmpllitinterp", Package: "html/template", Opaque: true}, // bug #66217: remove Opaque //{Name: "multipartfiles", Package: "mime/multipart"}, diff --git a/src/net/http/fs.go b/src/net/http/fs.go index 92bd94f72dfb685bf874d79f233c25cb91e66b48..e1ec926052230c2923dce2700bcaf5e5a1938357 100644 --- a/src/net/http/fs.go +++ b/src/net/http/fs.go @@ -12,6 +12,7 @@ "fmt" "internal/godebug" "io" "io/fs" + "math" "mime" "mime/multipart" "net/textproto" @@ -1010,6 +1011,25 @@ "Content-Type": {contentType}, } } +// GODEBUG=httpservecontentmaxranges= controls the maximum number of ranges that will +// be processed in a Range header. Setting httpservecontentmaxranges=0 disables the limit. +var httpservecontentmaxranges = godebug.New("httpservecontentmaxranges") + +const defaultMaxContentRanges = 200 + +func maxContentRanges() int { + maxRanges := defaultMaxContentRanges + if v := httpservecontentmaxranges.Value(); v != "" { + if n, err := strconv.Atoi(v); err == nil && n >= 0 { + maxRanges = n + if maxRanges == 0 { + maxRanges = math.MaxInt + } + } + } + return maxRanges +} + // parseRange parses a Range header string as per RFC 7233. // errNoOverlap is returned if none of the ranges overlap. func parseRange(s string, size int64) ([]httpRange, error) { @@ -1022,6 +1042,14 @@ return nil, errors.New("invalid range") } var ranges []httpRange noOverlap := false + numRanges := strings.Count(s[len(b):], ",") + 1 + maxRanges := maxContentRanges() + if (numRanges > maxRanges) != (numRanges > defaultMaxContentRanges) { + httpservecontentmaxranges.IncNonDefault() + } + if numRanges > maxRanges { + return nil, nil // ignore header with too many ranges + } for ra := range strings.SplitSeq(s[len(b):], ",") { ra = textproto.TrimString(ra) if ra == "" { diff --git a/src/net/http/range_test.go b/src/net/http/range_test.go index 114987ed2c6984946a1165324d74d16e9af914a2..6cfef250d52c133af7c995c07c1204ea5ec4f0ea 100644 --- a/src/net/http/range_test.go +++ b/src/net/http/range_test.go @@ -5,6 +5,8 @@ package http import ( + "runtime/metrics" + "strings" "testing" ) @@ -77,3 +79,100 @@ } } } } + +func TestParseRangeLimit(t *testing.T) { + for _, tc := range []struct { + name string + godebug string + numRanges int + want int + wantNonDef bool + }{ + { + name: "default limit not exceeded", + numRanges: defaultMaxContentRanges, + want: defaultMaxContentRanges, + }, + { + name: "default limit exceeded", + numRanges: defaultMaxContentRanges + 1, + want: 0, + }, + { + name: "small limit exceeded", + godebug: "httpservecontentmaxranges=10", + numRanges: 11, + want: 0, + wantNonDef: true, + }, + { + name: "small limit not exceeded", + godebug: "httpservecontentmaxranges=10", + numRanges: 10, + want: 10, + }, + { + name: "disabled limit", + godebug: "httpservecontentmaxranges=0", + numRanges: 2 * defaultMaxContentRanges, + want: 2 * defaultMaxContentRanges, + wantNonDef: true, + }, + { + name: "large limit exceeded", + godebug: "httpservecontentmaxranges=300", + numRanges: 301, + want: 0, + }, + { + name: "large limit not exceeded", + godebug: "httpservecontentmaxranges=300", + numRanges: 300, + want: 300, + wantNonDef: true, + }, + { + name: "large limit not exceeded by small input", + godebug: "httpservecontentmaxranges=300", + numRanges: 10, + want: 10, + }, + { + name: "invalid limit negative", + godebug: "httpservecontentmaxranges=-5", + numRanges: defaultMaxContentRanges + 1, + want: 0, + }, + { + name: "invalid limit non-numeric", + godebug: "httpservecontentmaxranges=abc", + numRanges: defaultMaxContentRanges + 1, + want: 0, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("GODEBUG", tc.godebug) + var m [1]metrics.Sample + m[0].Name = "/godebug/non-default-behavior/httpservecontentmaxranges:events" + metrics.Read(m[:]) + before := m[0].Value.Uint64() + + rangeHeader := "bytes=" + strings.Repeat("0-0,", tc.numRanges-1) + "0-0" + ranges, err := parseRange(rangeHeader, 10_000_000) + if err != nil { + t.Fatalf("parseRange(%q): %v", rangeHeader, err) + } + if got := len(ranges); got != tc.want { + t.Errorf("len(ranges) = %v, want %v", got, tc.want) + } + + metrics.Read(m[:]) + after := m[0].Value.Uint64() + if tc.wantNonDef && after <= before { + t.Errorf("metric did not increment: before=%d, after=%d", before, after) + } else if !tc.wantNonDef && after != before { + t.Errorf("metric unexpectedly incremented: before=%d, after=%d", before, after) + } + }) + } +} diff --git a/src/runtime/metrics/doc.go b/src/runtime/metrics/doc.go index 699b09fb7cd9ccaf0c5627cf89b385a5f47cafbb..1b40b34a346d5a23966ed45a85d413c1d69daa06 100644 --- a/src/runtime/metrics/doc.go +++ b/src/runtime/metrics/doc.go @@ -342,6 +342,11 @@ The number of non-default behaviors executed by the net/http package due to a non-default GODEBUG=httpservecontentkeepheaders=... setting. + /godebug/non-default-behavior/httpservecontentmaxranges:events + The number of non-default behaviors executed + by the net/http package due to a non-default + GODEBUG=httpservecontentmaxranges=... setting. + /godebug/non-default-behavior/installgoroot:events The number of non-default behaviors executed by the go/build package due to a non-default GODEBUG=installgoroot=... setting.