Skip to content

Commit d5734d4

Browse files
committed
net/http: only support "chunked" in inbound Transfer-Encoding headers
This is a security hardening measure against HTTP request smuggling. Thank you to ZeddYu for reporting this issue. We weren't parsing things correctly anyway, allowing "identity" to be combined with "chunked", and ignoring any Transfer-Encoding header past the first. This is a delicate security surface that already broke before, just be strict and don't add complexity to support cases not observed in the wild (nginx removed "identity" support [1] and multiple TE header support [2]) and removed by RFC 7230 (see page 81). It'd probably be good to also drop support for anything other than "chunked" in outbound TE headers, as "identity" is not a thing anymore, and we are probably off-spec for anything other than "chunked", but it should not be a security concern, so leaving it for now. See #38867. [1]: https://hg.nginx.org/nginx/rev/fe5976aae0e3 [2]: https://hg.nginx.org/nginx/rev/aca005d232ff Change-Id: If17d0827f9c6167a0b19a158e2bc5844ec803288 Reviewed-on: https://go-review.googlesource.com/c/go/+/231418 Reviewed-by: Katie Hockman <[email protected]>
1 parent 33249f4 commit d5734d4

File tree

4 files changed

+66
-97
lines changed

4 files changed

+66
-97
lines changed

src/net/http/response_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ func TestReadResponseCloseInMiddle(t *testing.T) {
734734
}
735735

736736
func diff(t *testing.T, prefix string, have, want interface{}) {
737+
t.Helper()
737738
hv := reflect.ValueOf(have).Elem()
738739
wv := reflect.ValueOf(want).Elem()
739740
if hv.Type() != wv.Type() {

src/net/http/serve_test.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,37 +1347,6 @@ func TestServerAllowsBlockingRemoteAddr(t *testing.T) {
13471347
}
13481348
}
13491349

1350-
func TestIdentityResponseHeaders(t *testing.T) {
1351-
// Not parallel; changes log output.
1352-
defer afterTest(t)
1353-
log.SetOutput(ioutil.Discard) // is noisy otherwise
1354-
defer log.SetOutput(os.Stderr)
1355-
1356-
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
1357-
w.Header().Set("Transfer-Encoding", "identity")
1358-
w.(Flusher).Flush()
1359-
fmt.Fprintf(w, "I am an identity response.")
1360-
}))
1361-
defer ts.Close()
1362-
1363-
c := ts.Client()
1364-
res, err := c.Get(ts.URL)
1365-
if err != nil {
1366-
t.Fatalf("Get error: %v", err)
1367-
}
1368-
defer res.Body.Close()
1369-
1370-
if g, e := res.TransferEncoding, []string(nil); !reflect.DeepEqual(g, e) {
1371-
t.Errorf("expected TransferEncoding of %v; got %v", e, g)
1372-
}
1373-
if _, haveCL := res.Header["Content-Length"]; haveCL {
1374-
t.Errorf("Unexpected Content-Length")
1375-
}
1376-
if !res.Close {
1377-
t.Errorf("expected Connection: close; got %v", res.Close)
1378-
}
1379-
}
1380-
13811350
// TestHeadResponses verifies that all MIME type sniffing and Content-Length
13821351
// counting of GET requests also happens on HEAD requests.
13831352
func TestHeadResponses_h1(t *testing.T) { testHeadResponses(t, h1Mode) }

src/net/http/transfer.go

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,11 @@ type transferReader struct {
425425
ProtoMajor int
426426
ProtoMinor int
427427
// Output
428-
Body io.ReadCloser
429-
ContentLength int64
430-
TransferEncoding []string
431-
Close bool
432-
Trailer Header
428+
Body io.ReadCloser
429+
ContentLength int64
430+
Chunked bool
431+
Close bool
432+
Trailer Header
433433
}
434434

435435
func (t *transferReader) protoAtLeast(m, n int) bool {
@@ -501,13 +501,12 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
501501
t.ProtoMajor, t.ProtoMinor = 1, 1
502502
}
503503

504-
// Transfer encoding, content length
505-
err = t.fixTransferEncoding()
506-
if err != nil {
504+
// Transfer-Encoding: chunked, and overriding Content-Length.
505+
if err := t.parseTransferEncoding(); err != nil {
507506
return err
508507
}
509508

510-
realLength, err := fixLength(isResponse, t.StatusCode, t.RequestMethod, t.Header, t.TransferEncoding)
509+
realLength, err := fixLength(isResponse, t.StatusCode, t.RequestMethod, t.Header, t.Chunked)
511510
if err != nil {
512511
return err
513512
}
@@ -522,7 +521,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
522521
}
523522

524523
// Trailer
525-
t.Trailer, err = fixTrailer(t.Header, t.TransferEncoding)
524+
t.Trailer, err = fixTrailer(t.Header, t.Chunked)
526525
if err != nil {
527526
return err
528527
}
@@ -532,9 +531,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
532531
// See RFC 7230, section 3.3.
533532
switch msg.(type) {
534533
case *Response:
535-
if realLength == -1 &&
536-
!chunked(t.TransferEncoding) &&
537-
bodyAllowedForStatus(t.StatusCode) {
534+
if realLength == -1 && !t.Chunked && bodyAllowedForStatus(t.StatusCode) {
538535
// Unbounded body.
539536
t.Close = true
540537
}
@@ -543,7 +540,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
543540
// Prepare body reader. ContentLength < 0 means chunked encoding
544541
// or close connection when finished, since multipart is not supported yet
545542
switch {
546-
case chunked(t.TransferEncoding):
543+
case t.Chunked:
547544
if noResponseBodyExpected(t.RequestMethod) || !bodyAllowedForStatus(t.StatusCode) {
548545
t.Body = NoBody
549546
} else {
@@ -569,13 +566,17 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
569566
case *Request:
570567
rr.Body = t.Body
571568
rr.ContentLength = t.ContentLength
572-
rr.TransferEncoding = t.TransferEncoding
569+
if t.Chunked {
570+
rr.TransferEncoding = []string{"chunked"}
571+
}
573572
rr.Close = t.Close
574573
rr.Trailer = t.Trailer
575574
case *Response:
576575
rr.Body = t.Body
577576
rr.ContentLength = t.ContentLength
578-
rr.TransferEncoding = t.TransferEncoding
577+
if t.Chunked {
578+
rr.TransferEncoding = []string{"chunked"}
579+
}
579580
rr.Close = t.Close
580581
rr.Trailer = t.Trailer
581582
}
@@ -605,8 +606,8 @@ func isUnsupportedTEError(err error) bool {
605606
return ok
606607
}
607608

608-
// fixTransferEncoding sanitizes t.TransferEncoding, if needed.
609-
func (t *transferReader) fixTransferEncoding() error {
609+
// parseTransferEncoding sets t.Chunked based on the Transfer-Encoding header.
610+
func (t *transferReader) parseTransferEncoding() error {
610611
raw, present := t.Header["Transfer-Encoding"]
611612
if !present {
612613
return nil
@@ -618,56 +619,38 @@ func (t *transferReader) fixTransferEncoding() error {
618619
return nil
619620
}
620621

621-
encodings := strings.Split(raw[0], ",")
622-
te := make([]string, 0, len(encodings))
623-
// TODO: Even though we only support "identity" and "chunked"
624-
// encodings, the loop below is designed with foresight. One
625-
// invariant that must be maintained is that, if present,
626-
// chunked encoding must always come first.
627-
for _, encoding := range encodings {
628-
encoding = strings.ToLower(strings.TrimSpace(encoding))
629-
// "identity" encoding is not recorded
630-
if encoding == "identity" {
631-
break
632-
}
633-
if encoding != "chunked" {
634-
return &unsupportedTEError{fmt.Sprintf("unsupported transfer encoding: %q", encoding)}
635-
}
636-
te = te[0 : len(te)+1]
637-
te[len(te)-1] = encoding
638-
}
639-
if len(te) > 1 {
640-
return badStringError("too many transfer encodings", strings.Join(te, ","))
641-
}
642-
if len(te) > 0 {
643-
// RFC 7230 3.3.2 says "A sender MUST NOT send a
644-
// Content-Length header field in any message that
645-
// contains a Transfer-Encoding header field."
646-
//
647-
// but also:
648-
// "If a message is received with both a
649-
// Transfer-Encoding and a Content-Length header
650-
// field, the Transfer-Encoding overrides the
651-
// Content-Length. Such a message might indicate an
652-
// attempt to perform request smuggling (Section 9.5)
653-
// or response splitting (Section 9.4) and ought to be
654-
// handled as an error. A sender MUST remove the
655-
// received Content-Length field prior to forwarding
656-
// such a message downstream."
657-
//
658-
// Reportedly, these appear in the wild.
659-
delete(t.Header, "Content-Length")
660-
t.TransferEncoding = te
661-
return nil
622+
// Like nginx, we only support a single Transfer-Encoding header field, and
623+
// only if set to "chunked". This is one of the most security sensitive
624+
// surfaces in HTTP/1.1 due to the risk of request smuggling, so we keep it
625+
// strict and simple.
626+
if len(raw) != 1 {
627+
return &unsupportedTEError{fmt.Sprintf("too many transfer encodings: %q", raw)}
628+
}
629+
if strings.ToLower(textproto.TrimString(raw[0])) != "chunked" {
630+
return &unsupportedTEError{fmt.Sprintf("unsupported transfer encoding: %q", raw[0])}
662631
}
663632

633+
// RFC 7230 3.3.2 says "A sender MUST NOT send a Content-Length header field
634+
// in any message that contains a Transfer-Encoding header field."
635+
//
636+
// but also: "If a message is received with both a Transfer-Encoding and a
637+
// Content-Length header field, the Transfer-Encoding overrides the
638+
// Content-Length. Such a message might indicate an attempt to perform
639+
// request smuggling (Section 9.5) or response splitting (Section 9.4) and
640+
// ought to be handled as an error. A sender MUST remove the received
641+
// Content-Length field prior to forwarding such a message downstream."
642+
//
643+
// Reportedly, these appear in the wild.
644+
delete(t.Header, "Content-Length")
645+
646+
t.Chunked = true
664647
return nil
665648
}
666649

667650
// Determine the expected body length, using RFC 7230 Section 3.3. This
668651
// function is not a method, because ultimately it should be shared by
669652
// ReadResponse and ReadRequest.
670-
func fixLength(isResponse bool, status int, requestMethod string, header Header, te []string) (int64, error) {
653+
func fixLength(isResponse bool, status int, requestMethod string, header Header, chunked bool) (int64, error) {
671654
isRequest := !isResponse
672655
contentLens := header["Content-Length"]
673656

@@ -711,7 +694,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
711694
}
712695

713696
// Logic based on Transfer-Encoding
714-
if chunked(te) {
697+
if chunked {
715698
return -1, nil
716699
}
717700

@@ -766,12 +749,12 @@ func shouldClose(major, minor int, header Header, removeCloseHeader bool) bool {
766749
}
767750

768751
// Parse the trailer header
769-
func fixTrailer(header Header, te []string) (Header, error) {
752+
func fixTrailer(header Header, chunked bool) (Header, error) {
770753
vv, ok := header["Trailer"]
771754
if !ok {
772755
return nil, nil
773756
}
774-
if !chunked(te) {
757+
if !chunked {
775758
// Trailer and no chunking:
776759
// this is an invalid use case for trailer header.
777760
// Nevertheless, no error will be returned and we

src/net/http/transfer_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ func TestTransferWriterWriteBodyReaderTypes(t *testing.T) {
279279
}
280280
}
281281

282-
func TestFixTransferEncoding(t *testing.T) {
282+
func TestParseTransferEncoding(t *testing.T) {
283283
tests := []struct {
284284
hdr Header
285285
wantErr error
@@ -290,7 +290,23 @@ func TestFixTransferEncoding(t *testing.T) {
290290
},
291291
{
292292
hdr: Header{"Transfer-Encoding": {"chunked, chunked", "identity", "chunked"}},
293-
wantErr: badStringError("too many transfer encodings", "chunked,chunked"),
293+
wantErr: &unsupportedTEError{`too many transfer encodings: ["chunked, chunked" "identity" "chunked"]`},
294+
},
295+
{
296+
hdr: Header{"Transfer-Encoding": {""}},
297+
wantErr: &unsupportedTEError{`unsupported transfer encoding: ""`},
298+
},
299+
{
300+
hdr: Header{"Transfer-Encoding": {"chunked, identity"}},
301+
wantErr: &unsupportedTEError{`unsupported transfer encoding: "chunked, identity"`},
302+
},
303+
{
304+
hdr: Header{"Transfer-Encoding": {"chunked", "identity"}},
305+
wantErr: &unsupportedTEError{`too many transfer encodings: ["chunked" "identity"]`},
306+
},
307+
{
308+
hdr: Header{"Transfer-Encoding": {"\x0bchunked"}},
309+
wantErr: &unsupportedTEError{`unsupported transfer encoding: "\vchunked"`},
294310
},
295311
{
296312
hdr: Header{"Transfer-Encoding": {"chunked"}},
@@ -304,7 +320,7 @@ func TestFixTransferEncoding(t *testing.T) {
304320
ProtoMajor: 1,
305321
ProtoMinor: 1,
306322
}
307-
gotErr := tr.fixTransferEncoding()
323+
gotErr := tr.parseTransferEncoding()
308324
if !reflect.DeepEqual(gotErr, tt.wantErr) {
309325
t.Errorf("%d.\ngot error:\n%v\nwant error:\n%v\n\n", i, gotErr, tt.wantErr)
310326
}

0 commit comments

Comments
 (0)