Skip to content

Commit a6642e6

Browse files
committed
net/textproto: reject invalid header keys/values in ReadMIMEHeader
Return an error when parsing a MIME header containing bytes in the key or value outside the set allowed by RFC 7230. For historical compatibility, accept spaces in keys (but do not canonicalize the key in this case). For #53188. Change-Id: I195319362a2fc69c4e506644f78c5026db070379 Reviewed-on: https://go-review.googlesource.com/c/go/+/410714 Reviewed-by: Brad Fitzpatrick <[email protected]> Reviewed-by: Bryan Mills <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Damien Neil <[email protected]>
1 parent 41be62e commit a6642e6

File tree

3 files changed

+135
-96
lines changed

3 files changed

+135
-96
lines changed

src/net/http/serve_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6165,7 +6165,7 @@ func testUnsupportedTransferEncodingsReturn501(t *testing.T, mode testMode) {
61656165
"fugazi",
61666166
"foo-bar",
61676167
"unknown",
6168-
"\rchunked",
6168+
`" chunked"`,
61696169
}
61706170

61716171
for _, badTE := range unsupportedTEs {

src/net/textproto/reader.go

+85-92
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,15 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
508508
if !ok {
509509
return m, ProtocolError("malformed MIME header line: " + string(kv))
510510
}
511-
key := canonicalMIMEHeaderKey(k)
511+
key, ok := canonicalMIMEHeaderKey(k)
512+
if !ok {
513+
return m, ProtocolError("malformed MIME header line: " + string(kv))
514+
}
515+
for _, c := range v {
516+
if !validHeaderValueByte(c) {
517+
return m, ProtocolError("malformed MIME header line: " + string(kv))
518+
}
519+
}
512520

513521
// As per RFC 7230 field-name is a token, tokens consist of one or more chars.
514522
// We could return a ProtocolError here, but better to be liberal in what we
@@ -585,10 +593,12 @@ func CanonicalMIMEHeaderKey(s string) string {
585593
return s
586594
}
587595
if upper && 'a' <= c && c <= 'z' {
588-
return canonicalMIMEHeaderKey([]byte(s))
596+
s, _ = canonicalMIMEHeaderKey([]byte(s))
597+
return s
589598
}
590599
if !upper && 'A' <= c && c <= 'Z' {
591-
return canonicalMIMEHeaderKey([]byte(s))
600+
s, _ = canonicalMIMEHeaderKey([]byte(s))
601+
return s
592602
}
593603
upper = c == '-'
594604
}
@@ -597,16 +607,66 @@ func CanonicalMIMEHeaderKey(s string) string {
597607

598608
const toLower = 'a' - 'A'
599609

600-
// validHeaderFieldByte reports whether b is a valid byte in a header
610+
// validHeaderFieldByte reports whether c is a valid byte in a header
601611
// field name. RFC 7230 says:
602612
//
603613
// header-field = field-name ":" OWS field-value OWS
604614
// field-name = token
605615
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
606616
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
607617
// token = 1*tchar
608-
func validHeaderFieldByte(b byte) bool {
609-
return int(b) < len(isTokenTable) && isTokenTable[b]
618+
func validHeaderFieldByte(c byte) bool {
619+
// mask is a 128-bit bitmap with 1s for allowed bytes,
620+
// so that the byte c can be tested with a shift and an and.
621+
// If c >= 128, then 1<<c and 1<<(c-64) will both be zero,
622+
// and this function will return false.
623+
const mask = 0 |
624+
(1<<(10)-1)<<'0' |
625+
(1<<(26)-1)<<'a' |
626+
(1<<(26)-1)<<'A' |
627+
1<<'!' |
628+
1<<'#' |
629+
1<<'$' |
630+
1<<'%' |
631+
1<<'&' |
632+
1<<'\'' |
633+
1<<'*' |
634+
1<<'+' |
635+
1<<'-' |
636+
1<<'.' |
637+
1<<'^' |
638+
1<<'_' |
639+
1<<'`' |
640+
1<<'|' |
641+
1<<'~'
642+
return ((uint64(1)<<c)&(mask&(1<<64-1)) |
643+
(uint64(1)<<(c-64))&(mask>>64)) != 0
644+
}
645+
646+
// validHeaderValueByte reports whether c is a valid byte in a header
647+
// field value. RFC 7230 says:
648+
//
649+
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
650+
// field-vchar = VCHAR / obs-text
651+
// obs-text = %x80-FF
652+
//
653+
// RFC 5234 says:
654+
//
655+
// HTAB = %x09
656+
// SP = %x20
657+
// VCHAR = %x21-7E
658+
func validHeaderValueByte(c byte) bool {
659+
// mask is a 128-bit bitmap with 1s for allowed bytes,
660+
// so that the byte c can be tested with a shift and an and.
661+
// If c >= 128, then 1<<c and 1<<(c-64) will both be zero.
662+
// Since this is the obs-text range, we invert the mask to
663+
// create a bitmap with 1s for disallowed bytes.
664+
const mask = 0 |
665+
(1<<(0x7f-0x21)-1)<<0x21 | // VCHAR: %x21-7E
666+
1<<0x20 | // SP: %x20
667+
1<<0x09 // HTAB: %x09
668+
return ((uint64(1)<<c)&^(mask&(1<<64-1)) |
669+
(uint64(1)<<(c-64))&^(mask>>64)) == 0
610670
}
611671

612672
// canonicalMIMEHeaderKey is like CanonicalMIMEHeaderKey but is
@@ -615,14 +675,29 @@ func validHeaderFieldByte(b byte) bool {
615675
//
616676
// For invalid inputs (if a contains spaces or non-token bytes), a
617677
// is unchanged and a string copy is returned.
618-
func canonicalMIMEHeaderKey(a []byte) string {
678+
//
679+
// ok is true if the header key contains only valid characters and spaces.
680+
// ReadMIMEHeader accepts header keys containing spaces, but does not
681+
// canonicalize them.
682+
func canonicalMIMEHeaderKey(a []byte) (_ string, ok bool) {
619683
// See if a looks like a header key. If not, return it unchanged.
684+
noCanon := false
620685
for _, c := range a {
621686
if validHeaderFieldByte(c) {
622687
continue
623688
}
624689
// Don't canonicalize.
625-
return string(a)
690+
if c == ' ' {
691+
// We accept invalid headers with a space before the
692+
// colon, but must not canonicalize them.
693+
// See https://go.dev/issue/34540.
694+
noCanon = true
695+
continue
696+
}
697+
return string(a), false
698+
}
699+
if noCanon {
700+
return string(a), true
626701
}
627702

628703
upper := true
@@ -644,9 +719,9 @@ func canonicalMIMEHeaderKey(a []byte) string {
644719
// case, so a copy of a's bytes into a new string does not
645720
// happen in this map lookup:
646721
if v := commonHeader[string(a)]; v != "" {
647-
return v
722+
return v, true
648723
}
649-
return string(a)
724+
return string(a), true
650725
}
651726

652727
// commonHeader interns common header strings.
@@ -700,85 +775,3 @@ func initCommonHeader() {
700775
commonHeader[v] = v
701776
}
702777
}
703-
704-
// isTokenTable is a copy of net/http/lex.go's isTokenTable.
705-
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
706-
var isTokenTable = [127]bool{
707-
'!': true,
708-
'#': true,
709-
'$': true,
710-
'%': true,
711-
'&': true,
712-
'\'': true,
713-
'*': true,
714-
'+': true,
715-
'-': true,
716-
'.': true,
717-
'0': true,
718-
'1': true,
719-
'2': true,
720-
'3': true,
721-
'4': true,
722-
'5': true,
723-
'6': true,
724-
'7': true,
725-
'8': true,
726-
'9': true,
727-
'A': true,
728-
'B': true,
729-
'C': true,
730-
'D': true,
731-
'E': true,
732-
'F': true,
733-
'G': true,
734-
'H': true,
735-
'I': true,
736-
'J': true,
737-
'K': true,
738-
'L': true,
739-
'M': true,
740-
'N': true,
741-
'O': true,
742-
'P': true,
743-
'Q': true,
744-
'R': true,
745-
'S': true,
746-
'T': true,
747-
'U': true,
748-
'W': true,
749-
'V': true,
750-
'X': true,
751-
'Y': true,
752-
'Z': true,
753-
'^': true,
754-
'_': true,
755-
'`': true,
756-
'a': true,
757-
'b': true,
758-
'c': true,
759-
'd': true,
760-
'e': true,
761-
'f': true,
762-
'g': true,
763-
'h': true,
764-
'i': true,
765-
'j': true,
766-
'k': true,
767-
'l': true,
768-
'm': true,
769-
'n': true,
770-
'o': true,
771-
'p': true,
772-
'q': true,
773-
'r': true,
774-
's': true,
775-
't': true,
776-
'u': true,
777-
'v': true,
778-
'w': true,
779-
'x': true,
780-
'y': true,
781-
'z': true,
782-
'|': true,
783-
'~': true,
784-
}

src/net/textproto/reader_test.go

+49-3
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,62 @@ func TestReadMIMEHeaderMalformed(t *testing.T) {
189189
"Foo-\r\n\tBar: foo\r\n\r\n",
190190
"Foo\r\n\t: foo\r\n\r\n",
191191
"Foo-\n\tBar",
192+
"Foo \tBar: foo\r\n\r\n",
192193
}
193-
194194
for _, input := range inputs {
195195
r := reader(input)
196-
if m, err := r.ReadMIMEHeader(); err == nil {
196+
if m, err := r.ReadMIMEHeader(); err == nil || err == io.EOF {
197197
t.Errorf("ReadMIMEHeader(%q) = %v, %v; want nil, err", input, m, err)
198198
}
199199
}
200200
}
201201

202+
func TestReadMIMEHeaderBytes(t *testing.T) {
203+
for i := 0; i <= 0xff; i++ {
204+
s := "Foo" + string(rune(i)) + "Bar: foo\r\n\r\n"
205+
r := reader(s)
206+
wantErr := true
207+
switch {
208+
case i >= '0' && i <= '9':
209+
wantErr = false
210+
case i >= 'a' && i <= 'z':
211+
wantErr = false
212+
case i >= 'A' && i <= 'Z':
213+
wantErr = false
214+
case i == '!' || i == '#' || i == '$' || i == '%' || i == '&' || i == '\'' || i == '*' || i == '+' || i == '-' || i == '.' || i == '^' || i == '_' || i == '`' || i == '|' || i == '~':
215+
wantErr = false
216+
case i == ':':
217+
// Special case: "Foo:Bar: foo" is the header "Foo".
218+
wantErr = false
219+
case i == ' ':
220+
wantErr = false
221+
}
222+
m, err := r.ReadMIMEHeader()
223+
if err != nil != wantErr {
224+
t.Errorf("ReadMIMEHeader(%q) = %v, %v; want error=%v", s, m, err, wantErr)
225+
}
226+
}
227+
for i := 0; i <= 0xff; i++ {
228+
s := "Foo: foo" + string(rune(i)) + "bar\r\n\r\n"
229+
r := reader(s)
230+
wantErr := true
231+
switch {
232+
case i >= 0x21 && i <= 0x7e:
233+
wantErr = false
234+
case i == ' ':
235+
wantErr = false
236+
case i == '\t':
237+
wantErr = false
238+
case i >= 0x80 && i <= 0xff:
239+
wantErr = false
240+
}
241+
m, err := r.ReadMIMEHeader()
242+
if (err != nil) != wantErr {
243+
t.Errorf("ReadMIMEHeader(%q) = %v, %v; want error=%v", s, m, err, wantErr)
244+
}
245+
}
246+
}
247+
202248
// Test that continued lines are properly trimmed. Issue 11204.
203249
func TestReadMIMEHeaderTrimContinued(t *testing.T) {
204250
// In this header, \n and \r\n terminated lines are mixed on purpose.
@@ -317,7 +363,7 @@ func TestCommonHeaders(t *testing.T) {
317363
b := []byte("content-Length")
318364
want := "Content-Length"
319365
n := testing.AllocsPerRun(200, func() {
320-
if x := canonicalMIMEHeaderKey(b); x != want {
366+
if x, _ := canonicalMIMEHeaderKey(b); x != want {
321367
t.Fatalf("canonicalMIMEHeaderKey(%q) = %q; want %q", b, x, want)
322368
}
323369
})

0 commit comments

Comments
 (0)