Commit 300d9a21 authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: harden Server against request smuggling

See RFC 7230.

Thanks to Régis Leroy for the report.

Change-Id: Ic1779bc2180900430d4d7a4938cac04ed73c304c
Reviewed-on: https://go-review.googlesource.com/11810Reviewed-by: default avatarRuss Cox <rsc@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
parent 2714005a
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/url" "net/url"
"reflect" "reflect"
"strings" "strings"
...@@ -323,6 +324,32 @@ var reqTests = []reqTest{ ...@@ -323,6 +324,32 @@ var reqTests = []reqTest{
noTrailer, noTrailer,
noError, noError,
}, },
// HEAD with Content-Length 0. Make sure this is permitted,
// since I think we used to send it.
{
"HEAD / HTTP/1.1\r\nHost: issue8261.com\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
&Request{
Method: "HEAD",
URL: &url.URL{
Path: "/",
},
Header: Header{
"Connection": []string{"close"},
"Content-Length": []string{"0"},
},
Host: "issue8261.com",
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Close: true,
RequestURI: "/",
},
noBody,
noTrailer,
noError,
},
} }
func TestReadRequest(t *testing.T) { func TestReadRequest(t *testing.T) {
...@@ -357,10 +384,38 @@ func TestReadRequest(t *testing.T) { ...@@ -357,10 +384,38 @@ func TestReadRequest(t *testing.T) {
} }
} }
func TestReadRequest_BadConnectHost(t *testing.T) { // reqBytes treats req as a request (with \n delimiters) and returns it with \r\n delimiters,
data := []byte("CONNECT []%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a HTTP/1.0\n\n") // ending in \r\n\r\n
r, err := ReadRequest(bufio.NewReader(bytes.NewReader(data))) func reqBytes(req string) []byte {
return []byte(strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n")
}
var badRequestTests = []struct {
name string
req []byte
}{
{"bad_connect_host", reqBytes("CONNECT []%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a HTTP/1.0")},
{"smuggle_two_contentlen", reqBytes(`POST / HTTP/1.1
Content-Length: 3
Content-Length: 4
abc`)},
{"smuggle_chunked_and_len", reqBytes(`POST / HTTP/1.1
Transfer-Encoding: chunked
Content-Length: 3
abc`)},
{"smuggle_content_len_head", reqBytes(`HEAD / HTTP/1.1
Host: foo
Content-Length: 5`)},
}
func TestReadRequest_Bad(t *testing.T) {
for _, tt := range badRequestTests {
got, err := ReadRequest(bufio.NewReader(bytes.NewReader(tt.req)))
if err == nil { if err == nil {
t.Fatal("Got unexpected request = %#v", r) all, err := ioutil.ReadAll(got.Body)
t.Errorf("%s: got unexpected request = %#v\n Body = %q, %v", tt.name, got, all, err)
}
} }
} }
...@@ -148,6 +148,9 @@ func (t *transferWriter) shouldSendContentLength() bool { ...@@ -148,6 +148,9 @@ func (t *transferWriter) shouldSendContentLength() bool {
return true return true
} }
if t.ContentLength == 0 && isIdentity(t.TransferEncoding) { if t.ContentLength == 0 && isIdentity(t.TransferEncoding) {
if t.Method == "GET" || t.Method == "HEAD" {
return false
}
return true return true
} }
...@@ -317,6 +320,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) { ...@@ -317,6 +320,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
} }
case *Request: case *Request:
t.Header = rr.Header t.Header = rr.Header
t.RequestMethod = rr.Method
t.ProtoMajor = rr.ProtoMajor t.ProtoMajor = rr.ProtoMajor
t.ProtoMinor = rr.ProtoMinor t.ProtoMinor = rr.ProtoMinor
// Transfer semantics for Requests are exactly like those for // Transfer semantics for Requests are exactly like those for
...@@ -333,7 +337,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) { ...@@ -333,7 +337,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
} }
// Transfer encoding, content length // Transfer encoding, content length
t.TransferEncoding, err = fixTransferEncoding(t.RequestMethod, t.Header) t.TransferEncoding, err = fixTransferEncoding(isResponse, t.RequestMethod, t.Header)
if err != nil { if err != nil {
return err return err
} }
...@@ -421,12 +425,12 @@ func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" } ...@@ -421,12 +425,12 @@ func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
func isIdentity(te []string) bool { return len(te) == 1 && te[0] == "identity" } func isIdentity(te []string) bool { return len(te) == 1 && te[0] == "identity" }
// Sanitize transfer encoding // Sanitize transfer encoding
func fixTransferEncoding(requestMethod string, header Header) ([]string, error) { func fixTransferEncoding(isResponse bool, requestMethod string, header Header) ([]string, error) {
raw, present := header["Transfer-Encoding"] raw, present := header["Transfer-Encoding"]
if !present { if !present {
return nil, nil return nil, nil
} }
isRequest := !isResponse
delete(header, "Transfer-Encoding") delete(header, "Transfer-Encoding")
encodings := strings.Split(raw[0], ",") encodings := strings.Split(raw[0], ",")
...@@ -451,10 +455,15 @@ func fixTransferEncoding(requestMethod string, header Header) ([]string, error) ...@@ -451,10 +455,15 @@ func fixTransferEncoding(requestMethod string, header Header) ([]string, error)
return nil, &badStringError{"too many transfer encodings", strings.Join(te, ",")} return nil, &badStringError{"too many transfer encodings", strings.Join(te, ",")}
} }
if len(te) > 0 { if len(te) > 0 {
// Chunked encoding trumps Content-Length. See RFC 2616 // RFC 7230 3.3.2 says "A sender MUST NOT send a
// Section 4.4. Currently len(te) > 0 implies chunked // Content-Length header field in any message that
// encoding. // contains a Transfer-Encoding header field."
if len(header["Content-Length"]) > 0 {
if isRequest {
return nil, errors.New("http: invalid Content-Length with Transfer-Encoding")
}
delete(header, "Content-Length") delete(header, "Content-Length")
}
return te, nil return te, nil
} }
...@@ -465,9 +474,17 @@ func fixTransferEncoding(requestMethod string, header Header) ([]string, error) ...@@ -465,9 +474,17 @@ func fixTransferEncoding(requestMethod string, header Header) ([]string, 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
// 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
// smuggling (RFC 7230), don't allow a Content-Length header for
// methods which don't permit bodies. As an exception, allow
// exactly one Content-Length header if its value is "0".
if isRequest && len(contentLens) > 0 && !(len(contentLens) == 1 && contentLens[0] == "0") {
return 0, fmt.Errorf("http: method cannot contain a Content-Length; got %q", contentLens)
}
return 0, nil return 0, nil
} }
if status/100 == 1 { if status/100 == 1 {
...@@ -478,13 +495,21 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header, ...@@ -478,13 +495,21 @@ 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
} }
// Logic based on Content-Length // Logic based on Content-Length
cl := strings.TrimSpace(header.get("Content-Length")) var cl string
if len(contentLens) == 1 {
cl = strings.TrimSpace(contentLens[0])
}
if cl != "" { if cl != "" {
n, err := parseContentLength(cl) n, err := parseContentLength(cl)
if err != nil { if err != nil {
...@@ -495,11 +520,14 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header, ...@@ -495,11 +520,14 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
header.Del("Content-Length") header.Del("Content-Length")
} }
if !isResponse && requestMethod == "GET" { if !isResponse {
// RFC 2616 doesn't explicitly permit nor forbid 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)
// if there's no mention of a body. // if there's no mention of a body.
// Likewise, all other request methods are assumed to have
// no body if neither Transfer-Encoding chunked nor a
// Content-Length are set.
return 0, nil return 0, nil
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment