On 10 August 2015 at 16:04, Tianon Gravi <admwig...@gmail.com> wrote: > Looks like we need to work on backporting three commits to 1.0.2, > 1.3.3, and 1.4.2: > > - https://github.com/golang/go/commit/117ddcb83d7f42d6aa72241240af99ded81118e9 > - https://github.com/golang/go/commit/300d9a21583e7cf0149a778a0611e76ff7c6680f > - https://github.com/golang/go/commit/143822585e32449860e624cace9d2e521deee62e
The attached patch is all three of these fix commits, and works as-is on 1.4.2. Will start working on a patch we can include in 1.3.3 next. ♥, - Tianon 4096R / B42F 6819 007F 00F8 8E36 4FD4 036A 9C25 BF35 7DD4
Index: golang/src/net/http/header.go =================================================================== --- golang.orig/src/net/http/header.go +++ golang/src/net/http/header.go @@ -168,6 +168,8 @@ func (h Header) WriteSubset(w io.Writer, // letter and any letter following a hyphen to upper case; // the rest are converted to lowercase. For example, the // canonical key for "accept-encoding" is "Accept-Encoding". +// If s contains a space or invalid header field bytes, it is +// returned without modifications. func CanonicalHeaderKey(s string) string { return textproto.CanonicalMIMEHeaderKey(s) } // hasToken reports whether token appears with v, ASCII Index: golang/src/net/textproto/reader.go =================================================================== --- golang.orig/src/net/textproto/reader.go +++ golang/src/net/textproto/reader.go @@ -540,11 +540,16 @@ func (r *Reader) upcomingHeaderNewlines( // the rest are converted to lowercase. For example, the // canonical key for "accept-encoding" is "Accept-Encoding". // MIME header keys are assumed to be ASCII only. +// If s contains a space or invalid header field bytes, it is +// returned without modifications. func CanonicalMIMEHeaderKey(s string) string { // Quick check for canonical encoding. upper := true for i := 0; i < len(s); i++ { c := s[i] + if !validHeaderFieldByte(c) { + return s + } if upper && 'a' <= c && c <= 'z' { return canonicalMIMEHeaderKey([]byte(s)) } @@ -558,19 +563,44 @@ func CanonicalMIMEHeaderKey(s string) st const toLower = 'a' - 'A' +// validHeaderFieldByte reports whether b is a valid byte in a header +// field key. This is actually stricter than RFC 7230, which says: +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +// token = 1*tchar +// TODO: revisit in Go 1.6+ and possibly expand this. But note that many +// servers have historically dropped '_' to prevent ambiguities when mapping +// to CGI environment variables. +func validHeaderFieldByte(b byte) bool { + return ('A' <= b && b <= 'Z') || + ('a' <= b && b <= 'z') || + ('0' <= b && b <= '9') || + b == '-' +} + // canonicalMIMEHeaderKey is like CanonicalMIMEHeaderKey but is // allowed to mutate the provided byte slice before returning the // string. +// +// For invalid inputs (if a contains spaces or non-token bytes), a +// is unchanged and a string copy is returned. func canonicalMIMEHeaderKey(a []byte) string { + // See if a looks like a header key. If not, return it unchanged. + for _, c := range a { + if validHeaderFieldByte(c) { + continue + } + // Don't canonicalize. + return string(a) + } + upper := true for i, c := range a { // Canonicalize: first letter upper case // and upper case after each dash. // (Host, User-Agent, If-Modified-Since). // MIME headers are ASCII only, so no Unicode issues. - if c == ' ' { - c = '-' - } else if upper && 'a' <= c && c <= 'z' { + if upper && 'a' <= c && c <= 'z' { c -= toLower } else if !upper && 'A' <= c && c <= 'Z' { c += toLower Index: golang/src/net/textproto/reader_test.go =================================================================== --- golang.orig/src/net/textproto/reader_test.go +++ golang/src/net/textproto/reader_test.go @@ -24,11 +24,14 @@ var canonicalHeaderKeyTests = []canonica {"uSER-aGENT", "User-Agent"}, {"user-agent", "User-Agent"}, {"USER-AGENT", "User-Agent"}, - {"üser-agenT", "üser-Agent"}, // non-ASCII unchanged + + // Non-ASCII or anything with spaces or non-token chars is unchanged: + {"üser-agenT", "üser-agenT"}, + {"a B", "a B"}, // This caused a panic due to mishandling of a space: - {"C Ontent-Transfer-Encoding", "C-Ontent-Transfer-Encoding"}, - {"foo bar", "Foo-Bar"}, + {"C Ontent-Transfer-Encoding", "C Ontent-Transfer-Encoding"}, + {"foo bar", "foo bar"}, } func TestCanonicalMIMEHeaderKey(t *testing.T) { @@ -185,7 +188,7 @@ func TestReadMIMEHeaderNonCompliant(t *t "Foo": {"bar"}, "Content-Language": {"en"}, "Sid": {"0"}, - "Audio-Mode": {"None"}, + "Audio Mode": {"None"}, "Privilege": {"127"}, } if !reflect.DeepEqual(m, want) || err != nil { Index: golang/src/net/http/readrequest_test.go =================================================================== --- golang.orig/src/net/http/readrequest_test.go +++ golang/src/net/http/readrequest_test.go @@ -9,6 +9,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "net/url" "reflect" "strings" @@ -177,6 +178,36 @@ var reqTests = []reqTest{ noError, }, + // Tests chunked body and a bogus Content-Length which should be deleted. + { + "POST / HTTP/1.1\r\n" + + "Host: foo.com\r\n" + + "Transfer-Encoding: chunked\r\n" + + "Content-Length: 9999\r\n\r\n" + // to be removed. + "3\r\nfoo\r\n" + + "3\r\nbar\r\n" + + "0\r\n" + + "\r\n", + &Request{ + Method: "POST", + URL: &url.URL{ + Path: "/", + }, + TransferEncoding: []string{"chunked"}, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: Header{}, + ContentLength: -1, + Host: "foo.com", + RequestURI: "/", + }, + + "foobar", + noTrailer, + noError, + }, + // CONNECT request with domain name: { "CONNECT www.google.com:443 HTTP/1.1\r\n\r\n", @@ -323,6 +354,32 @@ var reqTests = []reqTest{ noTrailer, 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) { @@ -356,3 +413,34 @@ func TestReadRequest(t *testing.T) { } } } + +// reqBytes treats req as a request (with \n delimiters) and returns it with \r\n delimiters, +// ending in \r\n\r\n +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_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 { + all, err := ioutil.ReadAll(got.Body) + t.Errorf("%s: got unexpected request = %#v\n Body = %q, %v", tt.name, got, all, err) + } + } +} Index: golang/src/net/http/transfer.go =================================================================== --- golang.orig/src/net/http/transfer.go +++ golang/src/net/http/transfer.go @@ -143,6 +143,9 @@ func (t *transferWriter) shouldSendConte return true } if t.ContentLength == 0 && isIdentity(t.TransferEncoding) { + if t.Method == "GET" || t.Method == "HEAD" { + return false + } return true } @@ -310,6 +313,7 @@ func readTransfer(msg interface{}, r *bu } case *Request: t.Header = rr.Header + t.RequestMethod = rr.Method t.ProtoMajor = rr.ProtoMajor t.ProtoMinor = rr.ProtoMinor // Transfer semantics for Requests are exactly like those for @@ -325,7 +329,7 @@ func readTransfer(msg interface{}, r *bu } // Transfer encoding, content length - t.TransferEncoding, err = fixTransferEncoding(t.RequestMethod, t.Header) + t.TransferEncoding, err = fixTransferEncoding(isResponse, t.RequestMethod, t.Header) if err != nil { return err } @@ -413,12 +417,11 @@ func chunked(te []string) bool { return func isIdentity(te []string) bool { return len(te) == 1 && te[0] == "identity" } // 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"] if !present { return nil, nil } - delete(header, "Transfer-Encoding") encodings := strings.Split(raw[0], ",") @@ -443,9 +446,22 @@ func fixTransferEncoding(requestMethod s return nil, &badStringError{"too many transfer encodings", strings.Join(te, ",")} } if len(te) > 0 { - // Chunked encoding trumps Content-Length. See RFC 2616 - // Section 4.4. Currently len(te) > 0 implies chunked - // encoding. + // RFC 7230 3.3.2 says "A sender MUST NOT send a + // Content-Length header field in any message that + // contains a Transfer-Encoding header field." + // + // but also: + // "If a message is received with both a + // Transfer-Encoding and a Content-Length header + // field, the Transfer-Encoding overrides the + // Content-Length. Such a message might indicate an + // attempt to perform request smuggling (Section 9.5) + // or response splitting (Section 9.4) and ought to be + // handled as an error. A sender MUST remove the + // received Content-Length field prior to forwarding + // such a message downstream." + // + // Reportedly, these appear in the wild. delete(header, "Content-Length") return te, nil } @@ -457,9 +473,17 @@ func fixTransferEncoding(requestMethod s // function is not a method, because ultimately it should be shared by // ReadResponse and ReadRequest. 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 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 } if status/100 == 1 { @@ -470,13 +494,21 @@ func fixLength(isResponse bool, status i 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 if chunked(te) { return -1, nil } // 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 != "" { n, err := parseContentLength(cl) if err != nil { @@ -487,11 +519,14 @@ func fixLength(isResponse bool, status i header.Del("Content-Length") } - if !isResponse && requestMethod == "GET" { - // RFC 2616 doesn't explicitly permit nor forbid an + if !isResponse { + // RFC 2616 neither explicitly permits nor forbids an // entity-body on a GET request so we permit one if // declared, but we default to 0 here (not -1 below) // 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 }