Skip to content

Commit 1c69384

Browse files
committed
net/textproto: reject all headers with a leading space
Previously, golang.org/cl/75350 updated ReadMIMEHeader to ignore the first header line when it begins with a leading space, as in the following example: GET / HTTP/1.1 Host: foo.com Accept-Encoding: gzip However, golang.org/cl/75350 changed ReadMIMEHeader's behavior for the following example: before the CL it returned an error, but after the CL it ignored the first line. GET / HTTP/1.1 Host foo.com Accept-Encoding: gzip This change updates ReadMIMEHeader to always fail when the first header line starts with a space. During the discussion for golang.org/cl/75350, we realized we had three competing needs: 1. HTTP clients should accept malformed response headers when possible (ignoring the malformed lines). 2. HTTP servers should reject all malformed request headers. 3. The net/textproto package is used by multiple protocols (most notably, HTTP and SMTP) which have slightly different parsing semantics. This complicates changes to net/textproto. We weren't sure how to best fix net/textproto without an API change, but it is too late for API changes in Go 1.10. We decided to ignore initial lines that begin with spaces, thinking that would have the least impact on existing users -- malformed headers would continue to parse, but the initial lines would be ignored. Instead, golang.org/cl/75350 actually changed ReadMIMEHeader to succeed in cases where it previously failed (as in the above example). Reconsidering the above two examples, there does not seem to be a good argument to silently ignore ` Host: foo.com` but fail on ` Host foo.com`. Hence, this change fails for *all* headers where the initial line begins with a space. Updates #22464 Change-Id: I68d3d190489c350b0bc1549735bf6593fe11a94c Reviewed-on: https://go-review.googlesource.com/80055 Run-TryBot: Tom Bergan <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent 671cf92 commit 1c69384

File tree

5 files changed

+55
-85
lines changed

5 files changed

+55
-85
lines changed

src/net/http/readrequest_test.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -401,26 +401,6 @@ var reqTests = []reqTest{
401401
noTrailer,
402402
noError,
403403
},
404-
405-
// leading whitespace in the first header. golang.org/issue/22464
406-
{
407-
"GET / HTTP/1.1\r\n Foobar: ignored\r\nConnection: close\r\n\r\n",
408-
&Request{
409-
Method: "GET",
410-
URL: &url.URL{
411-
Path: "/",
412-
},
413-
Header: Header{"Connection": {"close"}},
414-
Proto: "HTTP/1.1",
415-
ProtoMajor: 1,
416-
ProtoMinor: 1,
417-
RequestURI: "/",
418-
Close: true,
419-
},
420-
noBodyStr,
421-
noTrailer,
422-
noError,
423-
},
424404
}
425405

426406
func TestReadRequest(t *testing.T) {
@@ -473,6 +453,14 @@ Content-Length: 4
473453
abc`)},
474454
{"smuggle_content_len_head", reqBytes(`HEAD / HTTP/1.1
475455
Host: foo
456+
Content-Length: 5`)},
457+
458+
// golang.org/issue/22464
459+
{"leading_space_in_header", reqBytes(`HEAD / HTTP/1.1
460+
Host: foo
461+
Content-Length: 5`)},
462+
{"leading_tab_in_header", reqBytes(`HEAD / HTTP/1.1
463+
\tHost: foo
476464
Content-Length: 5`)},
477465
}
478466

src/net/http/response_test.go

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -555,28 +555,6 @@ some body`,
555555
},
556556
"Your Authentication failed.\r\n",
557557
},
558-
559-
// leading whitespace in the first header. golang.org/issue/22464
560-
{
561-
"HTTP/1.1 200 OK\r\n" +
562-
" Content-type: text/html\r\n" +
563-
"\tIgnore: foobar\r\n" +
564-
"Foo: bar\r\n\r\n",
565-
Response{
566-
Status: "200 OK",
567-
StatusCode: 200,
568-
Proto: "HTTP/1.1",
569-
ProtoMajor: 1,
570-
ProtoMinor: 1,
571-
Request: dummyReq("GET"),
572-
Header: Header{
573-
"Foo": {"bar"},
574-
},
575-
Close: true,
576-
ContentLength: -1,
577-
},
578-
"",
579-
},
580558
}
581559

582560
// tests successful calls to ReadResponse, and inspects the returned Response.
@@ -838,7 +816,6 @@ func TestReadResponseErrors(t *testing.T) {
838816
type testCase struct {
839817
name string // optional, defaults to in
840818
in string
841-
header Header
842819
wantErr interface{} // nil, err value, or string substring
843820
}
844821

@@ -864,22 +841,21 @@ func TestReadResponseErrors(t *testing.T) {
864841
}
865842
}
866843

867-
contentLength := func(status, body string, wantErr interface{}, header Header) testCase {
844+
contentLength := func(status, body string, wantErr interface{}) testCase {
868845
return testCase{
869846
name: fmt.Sprintf("status %q %q", status, body),
870847
in: fmt.Sprintf("HTTP/1.1 %s\r\n%s", status, body),
871848
wantErr: wantErr,
872-
header: header,
873849
}
874850
}
875851

876852
errMultiCL := "message cannot contain multiple Content-Length headers"
877853

878854
tests := []testCase{
879-
{"", "", nil, io.ErrUnexpectedEOF},
880-
{"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", nil, io.ErrUnexpectedEOF},
881-
{"", "HTTP/1.1", nil, "malformed HTTP response"},
882-
{"", "HTTP/2.0", nil, "malformed HTTP response"},
855+
{"", "", io.ErrUnexpectedEOF},
856+
{"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", io.ErrUnexpectedEOF},
857+
{"", "HTTP/1.1", "malformed HTTP response"},
858+
{"", "HTTP/2.0", "malformed HTTP response"},
883859
status("20X Unknown", true),
884860
status("abcd Unknown", true),
885861
status("二百/两百 OK", true),
@@ -905,18 +881,22 @@ func TestReadResponseErrors(t *testing.T) {
905881
version("HTTP/1", true),
906882
version("http/1.1", true),
907883

908-
contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL, nil),
909-
contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil, Header{"Content-Length": {"7"}}),
910-
contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL, nil),
911-
contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil, Header{"Content-Length": {"0"}}),
912-
contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil, nil),
913-
contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL, nil),
884+
contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL),
885+
contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil),
886+
contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL),
887+
contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil),
888+
contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil),
889+
contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL),
914890

915891
// multiple content-length headers for 204 and 304 should still be checked
916-
contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL, nil),
917-
contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil, nil),
918-
contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL, nil),
919-
contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil, nil),
892+
contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL),
893+
contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil),
894+
contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL),
895+
contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil),
896+
897+
// golang.org/issue/22464
898+
{"leading space in header", "HTTP/1.1 200 OK\r\n Content-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
899+
{"leading tab in header", "HTTP/1.1 200 OK\r\n\tContent-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
920900
}
921901

922902
for i, tt := range tests {

src/net/http/transport_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4281,12 +4281,13 @@ func TestMissingStatusNoPanic(t *testing.T) {
42814281
shutdown := make(chan bool, 1)
42824282
done := make(chan bool)
42834283
fullAddrURL := fmt.Sprintf("http://%s", addr)
4284-
raw := `HTTP/1.1 400
4285-
Date: Wed, 30 Aug 2017 19:09:27 GMT
4286-
Content-Type: text/html; charset=utf-8
4287-
Content-Length: 10
4288-
Last-Modified: Wed, 30 Aug 2017 19:02:02 GMT
4289-
Vary: Accept-Encoding` + "\r\n\r\nAloha Olaa"
4284+
raw := "HTTP/1.1 400\r\n" +
4285+
"Date: Wed, 30 Aug 2017 19:09:27 GMT\r\n" +
4286+
"Content-Type: text/html; charset=utf-8\r\n" +
4287+
"Content-Length: 10\r\n" +
4288+
"Last-Modified: Wed, 30 Aug 2017 19:02:02 GMT\r\n" +
4289+
"Vary: Accept-Encoding\r\n\r\n" +
4290+
"Aloha Olaa"
42904291

42914292
go func() {
42924293
defer func() {

src/net/textproto/reader.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -477,11 +477,13 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
477477

478478
m := make(MIMEHeader, hint)
479479

480-
for r.skipSpace() > 0 {
480+
// The first line cannot start with a leading space.
481+
if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') {
481482
line, err := r.readLineSlice()
482-
if len(line) == 0 || err != nil {
483+
if err != nil {
483484
return m, err
484485
}
486+
return m, ProtocolError("malformed MIME header initial line: " + string(line))
485487
}
486488

487489
for {
@@ -490,9 +492,9 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
490492
return m, err
491493
}
492494

493-
// Key ends at first colon; should not have spaces but
494-
// they appear in the wild, violating specs, so we
495-
// remove them if present.
495+
// Key ends at first colon; should not have trailing spaces
496+
// but they appear in the wild, violating specs, so we remove
497+
// them if present.
496498
i := bytes.IndexByte(kv, ':')
497499
if i < 0 {
498500
return m, ProtocolError("malformed MIME header line: " + string(kv))

src/net/textproto/reader_test.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -211,21 +211,20 @@ func TestReadMIMEHeaderNonCompliant(t *testing.T) {
211211
}
212212
}
213213

214-
func TestReadMIMEHeaderLeadingSpace(t *testing.T) {
215-
tests := []struct {
216-
input string
217-
want MIMEHeader
218-
}{
219-
{" Ignore: ignore\r\nFoo: foo\r\n\r\n", MIMEHeader{"Foo": {"foo"}}},
220-
{"\tIgnore: ignore\r\nFoo: foo\r\n\r\n", MIMEHeader{"Foo": {"foo"}}},
221-
{" Ignore1: ignore\r\n Ignore2: ignore\r\nFoo: foo\r\n\r\n", MIMEHeader{"Foo": {"foo"}}},
222-
{" Ignore1: ignore\r\n\r\n", MIMEHeader{}},
223-
}
224-
for _, tt := range tests {
225-
r := reader(tt.input)
226-
m, err := r.ReadMIMEHeader()
227-
if !reflect.DeepEqual(m, tt.want) || err != nil {
228-
t.Errorf("ReadMIMEHeader(%q) = %v, %v; want %v", tt.input, m, err, tt.want)
214+
func TestReadMIMEHeaderMalformed(t *testing.T) {
215+
inputs := []string{
216+
"No colon first line\r\nFoo: foo\r\n\r\n",
217+
" No colon first line with leading space\r\nFoo: foo\r\n\r\n",
218+
"\tNo colon first line with leading tab\r\nFoo: foo\r\n\r\n",
219+
" First: line with leading space\r\nFoo: foo\r\n\r\n",
220+
"\tFirst: line with leading tab\r\nFoo: foo\r\n\r\n",
221+
"Foo: foo\r\nNo colon second line\r\n\r\n",
222+
}
223+
224+
for _, input := range inputs {
225+
r := reader(input)
226+
if m, err := r.ReadMIMEHeader(); err == nil {
227+
t.Errorf("ReadMIMEHeader(%q) = %v, %v; want nil, err", input, m, err)
229228
}
230229
}
231230
}

0 commit comments

Comments
 (0)