Skip to content

Commit

Permalink
net/textproto: don't treat spaces as hyphens in header keys
Browse files Browse the repository at this point in the history
This was originally done in https://codereview.appspot.com/5690059
(Feb 2012) to deal with bad response headers coming back from webcams,
but it presents a potential security problem with HTTP request
smuggling for request headers containing "Content Length" instead of
"Content-Length".

Part of overall HTTP hardening for request smuggling. See RFC 7230.

Thanks to Régis Leroy for the report.

Change-Id: I92b17fb637c9171c5774ea1437979ae2c17ca88a
Reviewed-on: https://go-review.googlesource.com/11772
Reviewed-by: Russ Cox <rsc@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
  • Loading branch information
bradfitz committed Jun 30, 2015
1 parent 8884fa7 commit 117ddcb
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/net/http/header.go
Expand Up @@ -168,6 +168,8 @@ func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error {
// 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
Expand Down
36 changes: 33 additions & 3 deletions src/net/textproto/reader.go
Expand Up @@ -547,11 +547,16 @@ func (r *Reader) upcomingHeaderNewlines() (n int) {
// 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))
}
Expand All @@ -565,19 +570,44 @@ func CanonicalMIMEHeaderKey(s string) string {

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
Expand Down
11 changes: 7 additions & 4 deletions src/net/textproto/reader_test.go
Expand Up @@ -24,11 +24,14 @@ var canonicalHeaderKeyTests = []canonicalHeaderKeyTest{
{"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) {
Expand Down Expand Up @@ -194,7 +197,7 @@ func TestReadMIMEHeaderNonCompliant(t *testing.T) {
"Foo": {"bar"},
"Content-Language": {"en"},
"Sid": {"0"},
"Audio-Mode": {"None"},
"Audio Mode": {"None"},
"Privilege": {"127"},
}
if !reflect.DeepEqual(m, want) || err != nil {
Expand Down

0 comments on commit 117ddcb

Please sign in to comment.