mirror of
https://github.com/golang/go
synced 2024-11-05 17:26:11 -07:00
net/http: support multiple identical Content-Length headers
Referencing RFC 7230 Section 3.3.2, this CL deduplicates multiple identical Content-Length headers of a message or rejects the message as invalid if the Content-Length values differ. Fixes #16490 Change-Id: Ia6b0f58ec7d35710b11a36113d2bd9128f693f64 Reviewed-on: https://go-review.googlesource.com/31252 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
516001f50f
commit
fe4307f060
@ -365,18 +365,68 @@ func TestFormFileOrder(t *testing.T) {
|
|||||||
|
|
||||||
var readRequestErrorTests = []struct {
|
var readRequestErrorTests = []struct {
|
||||||
in string
|
in string
|
||||||
err error
|
err string
|
||||||
|
|
||||||
|
header Header
|
||||||
}{
|
}{
|
||||||
{"GET / HTTP/1.1\r\nheader:foo\r\n\r\n", nil},
|
0: {"GET / HTTP/1.1\r\nheader:foo\r\n\r\n", "", Header{"Header": {"foo"}}},
|
||||||
{"GET / HTTP/1.1\r\nheader:foo\r\n", io.ErrUnexpectedEOF},
|
1: {"GET / HTTP/1.1\r\nheader:foo\r\n", io.ErrUnexpectedEOF.Error(), nil},
|
||||||
{"", io.EOF},
|
2: {"", io.EOF.Error(), nil},
|
||||||
|
3: {
|
||||||
|
in: "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n",
|
||||||
|
err: "http: method cannot contain a Content-Length",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
in: "HEAD / HTTP/1.1\r\n\r\n",
|
||||||
|
header: Header{},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Multiple Content-Length values should either be
|
||||||
|
// deduplicated if same or reject otherwise
|
||||||
|
// See Issue 16490.
|
||||||
|
5: {
|
||||||
|
in: "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 0\r\n\r\nGopher hey\r\n",
|
||||||
|
err: "cannot contain multiple Content-Length headers",
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
in: "POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Length: 6\r\n\r\nGopher\r\n",
|
||||||
|
err: "cannot contain multiple Content-Length headers",
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
in: "PUT / HTTP/1.1\r\nContent-Length: 6 \r\nContent-Length: 6\r\nContent-Length:6\r\n\r\nGopher\r\n",
|
||||||
|
err: "",
|
||||||
|
header: Header{"Content-Length": {"6"}},
|
||||||
|
},
|
||||||
|
8: {
|
||||||
|
in: "PUT / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 6 \r\n\r\n",
|
||||||
|
err: "cannot contain multiple Content-Length headers",
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
in: "POST / HTTP/1.1\r\nContent-Length:\r\nContent-Length: 3\r\n\r\n",
|
||||||
|
err: "cannot contain multiple Content-Length headers",
|
||||||
|
},
|
||||||
|
10: {
|
||||||
|
in: "HEAD / HTTP/1.1\r\nContent-Length:0\r\nContent-Length: 0\r\n\r\n",
|
||||||
|
header: Header{"Content-Length": {"0"}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadRequestErrors(t *testing.T) {
|
func TestReadRequestErrors(t *testing.T) {
|
||||||
for i, tt := range readRequestErrorTests {
|
for i, tt := range readRequestErrorTests {
|
||||||
_, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.in)))
|
req, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.in)))
|
||||||
if err != tt.err {
|
if err == nil {
|
||||||
t.Errorf("%d. got error = %v; want %v", i, err, tt.err)
|
if tt.err != "" {
|
||||||
|
t.Errorf("#%d: got nil err; want %q", i, tt.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tt.header, req.Header) {
|
||||||
|
t.Errorf("#%d: gotHeader: %q wantHeader: %q", i, req.Header, tt.header)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.err == "" || !strings.Contains(err.Error(), tt.err) {
|
||||||
|
t.Errorf("%d: got error = %v; want %v", i, err, tt.err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -792,6 +792,7 @@ func TestReadResponseErrors(t *testing.T) {
|
|||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string // optional, defaults to in
|
name string // optional, defaults to in
|
||||||
in string
|
in string
|
||||||
|
header Header
|
||||||
wantErr interface{} // nil, err value, or string substring
|
wantErr interface{} // nil, err value, or string substring
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,11 +818,22 @@ func TestReadResponseErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentLength := func(status, body string, wantErr interface{}, header Header) testCase {
|
||||||
|
return testCase{
|
||||||
|
name: fmt.Sprintf("status %q %q", status, body),
|
||||||
|
in: fmt.Sprintf("HTTP/1.1 %s\r\n%s", status, body),
|
||||||
|
wantErr: wantErr,
|
||||||
|
header: header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errMultiCL := "message cannot contain multiple Content-Length headers"
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
{"", "", io.ErrUnexpectedEOF},
|
{"", "", nil, io.ErrUnexpectedEOF},
|
||||||
{"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", io.ErrUnexpectedEOF},
|
{"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", nil, io.ErrUnexpectedEOF},
|
||||||
{"", "HTTP/1.1", "malformed HTTP response"},
|
{"", "HTTP/1.1", nil, "malformed HTTP response"},
|
||||||
{"", "HTTP/2.0", "malformed HTTP response"},
|
{"", "HTTP/2.0", nil, "malformed HTTP response"},
|
||||||
status("20X Unknown", true),
|
status("20X Unknown", true),
|
||||||
status("abcd Unknown", true),
|
status("abcd Unknown", true),
|
||||||
status("二百/两百 OK", true),
|
status("二百/两百 OK", true),
|
||||||
@ -846,7 +858,21 @@ func TestReadResponseErrors(t *testing.T) {
|
|||||||
version("HTTP/A.B", true),
|
version("HTTP/A.B", true),
|
||||||
version("HTTP/1", true),
|
version("HTTP/1", true),
|
||||||
version("http/1.1", true),
|
version("http/1.1", true),
|
||||||
|
|
||||||
|
contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL, nil),
|
||||||
|
contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil, Header{"Content-Length": {"7"}}),
|
||||||
|
contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL, nil),
|
||||||
|
contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil, Header{"Content-Length": {"0"}}),
|
||||||
|
contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil, nil),
|
||||||
|
contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL, nil),
|
||||||
|
|
||||||
|
// multiple content-length headers for 204 and 304 should still be checked
|
||||||
|
contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL, nil),
|
||||||
|
contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil, nil),
|
||||||
|
contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL, nil),
|
||||||
|
contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil, nil),
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
br := bufio.NewReader(strings.NewReader(tt.in))
|
br := bufio.NewReader(strings.NewReader(tt.in))
|
||||||
_, rerr := ReadResponse(br, nil)
|
_, rerr := ReadResponse(br, nil)
|
||||||
|
@ -473,8 +473,29 @@ func (t *transferReader) fixTransferEncoding() error {
|
|||||||
// function is not a method, because ultimately it should be shared by
|
// function is not a method, because ultimately it should be shared by
|
||||||
// ReadResponse and ReadRequest.
|
// ReadResponse and ReadRequest.
|
||||||
func fixLength(isResponse bool, status int, requestMethod string, header Header, te []string) (int64, error) {
|
func fixLength(isResponse bool, status int, requestMethod string, header Header, te []string) (int64, error) {
|
||||||
contentLens := header["Content-Length"]
|
|
||||||
isRequest := !isResponse
|
isRequest := !isResponse
|
||||||
|
contentLens := header["Content-Length"]
|
||||||
|
|
||||||
|
// Hardening against HTTP request smuggling
|
||||||
|
if len(contentLens) > 1 {
|
||||||
|
// Per RFC 7230 Section 3.3.2, prevent multiple
|
||||||
|
// Content-Length headers if they differ in value.
|
||||||
|
// If there are dups of the value, remove the dups.
|
||||||
|
// See Issue 16490.
|
||||||
|
first := strings.TrimSpace(contentLens[0])
|
||||||
|
for _, ct := range contentLens[1:] {
|
||||||
|
if first != strings.TrimSpace(ct) {
|
||||||
|
return 0, fmt.Errorf("http: message cannot contain multiple Content-Length headers; got %q", contentLens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deduplicate Content-Length
|
||||||
|
header.Del("Content-Length")
|
||||||
|
header.Add("Content-Length", first)
|
||||||
|
|
||||||
|
contentLens = header["Content-Length"]
|
||||||
|
}
|
||||||
|
|
||||||
// Logic based on response type or status
|
// Logic based on response type or status
|
||||||
if noBodyExpected(requestMethod) {
|
if noBodyExpected(requestMethod) {
|
||||||
// For HTTP requests, as part of hardening against request
|
// For HTTP requests, as part of hardening against request
|
||||||
@ -494,11 +515,6 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(contentLens) > 1 {
|
|
||||||
// harden against HTTP request smuggling. See RFC 7230.
|
|
||||||
return 0, errors.New("http: message cannot contain multiple Content-Length headers")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logic based on Transfer-Encoding
|
// Logic based on Transfer-Encoding
|
||||||
if chunked(te) {
|
if chunked(te) {
|
||||||
return -1, nil
|
return -1, nil
|
||||||
@ -519,7 +535,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
|
|||||||
header.Del("Content-Length")
|
header.Del("Content-Length")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isResponse {
|
if isRequest {
|
||||||
// RFC 2616 neither explicitly permits nor forbids an
|
// RFC 2616 neither explicitly permits nor forbids an
|
||||||
// entity-body on a GET request so we permit one if
|
// entity-body on a GET request so we permit one if
|
||||||
// declared, but we default to 0 here (not -1 below)
|
// declared, but we default to 0 here (not -1 below)
|
||||||
|
Loading…
Reference in New Issue
Block a user