net/http: disallow empty Content-Length header

The Content-Length must be a valid numeric value, empty values should
not be accepted.

See: https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length

Fixes #61679
This commit is contained in:
Mauri de Souza Meneguzzo 2023-08-01 09:07:44 -03:00
parent 162469b3cf
commit 932e46b55b
6 changed files with 41 additions and 16 deletions

View File

@ -134,6 +134,10 @@ The default is tlsmaxrsasize=8192, limiting RSA to 8192-bit keys. To avoid
denial of service attacks, this setting and default was backported to Go denial of service attacks, this setting and default was backported to Go
1.19.13, Go 1.20.8, and Go 1.21.1. 1.19.13, Go 1.20.8, and Go 1.21.1.
Go 1.22 made it an error for a request or response read by a net/http
client or server to have an empty Content-Length header.
This behavior is controlled by the `httplaxcontentlength` setting.
### Go 1.21 ### Go 1.21
Go 1.21 made it a run-time error to call `panic` with a nil interface value, Go 1.21 made it a run-time error to call `panic` with a nil interface value,

View File

@ -32,6 +32,7 @@ var All = []Info{
{Name: "http2client", Package: "net/http"}, {Name: "http2client", Package: "net/http"},
{Name: "http2debug", Package: "net/http", Opaque: true}, {Name: "http2debug", Package: "net/http", Opaque: true},
{Name: "http2server", Package: "net/http"}, {Name: "http2server", Package: "net/http"},
{Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},
{Name: "installgoroot", Package: "go/build"}, {Name: "installgoroot", Package: "go/build"},
{Name: "jstmpllitinterp", Package: "html/template"}, {Name: "jstmpllitinterp", Package: "html/template"},
//{Name: "multipartfiles", Package: "mime/multipart"}, //{Name: "multipartfiles", Package: "mime/multipart"},

View File

@ -883,6 +883,7 @@ func TestReadResponseErrors(t *testing.T) {
} }
errMultiCL := "message cannot contain multiple Content-Length headers" errMultiCL := "message cannot contain multiple Content-Length headers"
errEmptyCL := "invalid empty Content-Length"
tests := []testCase{ tests := []testCase{
{"", "", io.ErrUnexpectedEOF}, {"", "", io.ErrUnexpectedEOF},
@ -918,7 +919,7 @@ func TestReadResponseErrors(t *testing.T) {
contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil), contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil),
contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL), contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL),
contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil), contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil),
contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil), contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", errEmptyCL),
contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL), contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL),
// multiple content-length headers for 204 and 304 should still be checked // multiple content-length headers for 204 and 304 should still be checked

View File

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"internal/godebug"
"io" "io"
"net/http/httptrace" "net/http/httptrace"
"net/http/internal" "net/http/internal"
@ -527,7 +528,7 @@ func readTransfer(msg any, r *bufio.Reader) (err error) {
return err return err
} }
if isResponse && t.RequestMethod == "HEAD" { if isResponse && t.RequestMethod == "HEAD" {
if n, err := parseContentLength(t.Header.get("Content-Length")); err != nil { if n, err := parseContentLength(t.Header["Content-Length"]); err != nil {
return err return err
} else { } else {
t.ContentLength = n t.ContentLength = n
@ -707,18 +708,15 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
return -1, nil return -1, nil
} }
if len(contentLens) > 0 {
// Logic based on Content-Length // Logic based on Content-Length
var cl string n, err := parseContentLength(contentLens)
if len(contentLens) == 1 {
cl = textproto.TrimString(contentLens[0])
}
if cl != "" {
n, err := parseContentLength(cl)
if err != nil { if err != nil {
return -1, err return -1, err
} }
return n, nil return n, nil
} }
header.Del("Content-Length") header.Del("Content-Length")
if isRequest { if isRequest {
@ -1038,19 +1036,31 @@ func (bl bodyLocked) Read(p []byte) (n int, err error) {
return bl.b.readLocked(p) return bl.b.readLocked(p)
} }
// parseContentLength trims whitespace from s and returns -1 if no value var laxContentLength = godebug.New("httplaxcontentlength")
// is set, or the value if it's >= 0.
func parseContentLength(cl string) (int64, error) { // parseContentLength checks that the header is valid and then trims
cl = textproto.TrimString(cl) // whitespace. It returns -1 if no value is set otherwise the value
if cl == "" { // if it's >= 0.
func parseContentLength(clHeaders []string) (int64, error) {
if len(clHeaders) == 0 {
return -1, nil return -1, nil
} }
cl := textproto.TrimString(clHeaders[0])
// The Content-Length must be a valid numeric value.
// See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13
if cl == "" {
if laxContentLength.Value() == "1" {
laxContentLength.IncNonDefault()
return -1, nil
}
return 0, badStringError("invalid empty Content-Length", cl)
}
n, err := strconv.ParseUint(cl, 10, 63) n, err := strconv.ParseUint(cl, 10, 63)
if err != nil { if err != nil {
return 0, badStringError("bad Content-Length", cl) return 0, badStringError("bad Content-Length", cl)
} }
return int64(n), nil return int64(n), nil
} }
// finishAsyncByteRead finishes reading the 1-byte sniff // finishAsyncByteRead finishes reading the 1-byte sniff

View File

@ -332,6 +332,10 @@ func TestParseContentLength(t *testing.T) {
cl string cl string
wantErr error wantErr error
}{ }{
{
cl: "",
wantErr: badStringError("invalid empty Content-Length", ""),
},
{ {
cl: "3", cl: "3",
wantErr: nil, wantErr: nil,
@ -356,7 +360,7 @@ func TestParseContentLength(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
if _, gotErr := parseContentLength(tt.cl); !reflect.DeepEqual(gotErr, tt.wantErr) { if _, gotErr := parseContentLength([]string{tt.cl}); !reflect.DeepEqual(gotErr, tt.wantErr) {
t.Errorf("%q:\n\tgot=%v\n\twant=%v", tt.cl, gotErr, tt.wantErr) t.Errorf("%q:\n\tgot=%v\n\twant=%v", tt.cl, gotErr, tt.wantErr)
} }
} }

View File

@ -254,6 +254,11 @@ Below is the full list of supported metrics, ordered lexicographically.
The number of non-default behaviors executed by the net/http The number of non-default behaviors executed by the net/http
package due to a non-default GODEBUG=http2server=... setting. package due to a non-default GODEBUG=http2server=... setting.
/godebug/non-default-behavior/httplaxcontentlength:events
The number of non-default behaviors executed by the net/http
package due to a non-default GODEBUG=httplaxcontentlength=...
setting.
/godebug/non-default-behavior/installgoroot:events /godebug/non-default-behavior/installgoroot:events
The number of non-default behaviors executed by the go/build The number of non-default behaviors executed by the go/build
package due to a non-default GODEBUG=installgoroot=... setting. package due to a non-default GODEBUG=installgoroot=... setting.