header: reject pre-colon whitespace in request headers (#2187)

Reject request header field names with whitespace immediately before the
colon instead of trimming them before special-header handling.

This prevents parser differentials for malformed framing and routing
headers such as Content-Length, Transfer-Encoding, and Host when a frontend
forwards raw invalid request headers.

Keep the existing response and trailer compatibility behavior unchanged, and
add regression coverage for both header-only parsing and full request body
reads.
This commit is contained in:
Erik Dubbelboer
2026-04-27 12:28:18 +09:00
committed by GitHub
parent 52131689e9
commit b8d29bee6e
3 changed files with 24 additions and 17 deletions
+5 -2
View File
@@ -3119,9 +3119,12 @@ func (h *RequestHeader) parseHeaders(buf []byte) (int, error) {
s.b = buf
for s.next() {
// Trim trailing whitespace before the colon to normalize headers
// like "Content-Length :" to "Content-Length:".
key := s.key
s.key = trimTrailingSpace(s.key)
if len(s.key) != len(key) {
h.connectionClose = true
return 0, fmt.Errorf("invalid header key %q", key)
}
if len(s.key) == 0 {
h.connectionClose = true
+6
View File
@@ -3372,6 +3372,12 @@ func TestRequestHeaderReadError(t *testing.T) {
// Space before header name
testRequestHeaderReadError(t, h, "G(ET /foo/bar HTTP/1.1\r\n foo: bar\r\n\r\n")
// Whitespace before the colon in request header fields
testRequestHeaderReadError(t, h, "GET /foo/bar HTTP/1.1\r\nHost: aaa.com\r\nFoo : bar\r\n\r\n")
testRequestHeaderReadError(t, h, "GET /foo/bar HTTP/1.1\r\nHost : aaa.com\r\n\r\n")
testRequestHeaderReadError(t, h, "POST /foo/bar HTTP/1.1\r\nHost: aaa.com\r\nContent-Length : 4\r\n\r\ntest")
testRequestHeaderReadError(t, h, "POST /foo/bar HTTP/1.1\r\nHost: aaa.com\r\nTransfer-Encoding : chunked\r\n\r\n4\r\ntest\r\n0\r\n\r\n")
// Duplicate host header
testRequestHeaderReadError(t, h, "GET /foo/bar HTTP/1.1\r\nHost: aaa.com\r\nhost: bbb.com\r\n\r\n")
+13 -15
View File
@@ -1777,25 +1777,23 @@ func TestRequestReadLimitBody(t *testing.T) {
testRequestReadLimitBodySuccess(t, "GET /foo HTTP/1.0\r\n\r\n", 0)
}
func TestRequestReadLimitBodyWhitespaceBeforeColonFramingHeaders(t *testing.T) {
func TestRequestReadLimitBodyRejectWhitespaceBeforeColonFramingHeaders(t *testing.T) {
t.Parallel()
var req Request
r := bytes.NewBufferString("POST /foo HTTP/1.1\r\nHost: a.com\r\nContent-Length : 4\r\n\r\ntestNEXT")
br := bufio.NewReader(r)
if err := req.ReadLimitBody(br, 10); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := string(req.Body()); got != "test" {
t.Fatalf("unexpected body %q", got)
tests := []string{
"POST /foo HTTP/1.1\r\nHost: a.com\r\nContent-Length : 4\r\n\r\ntestNEXT",
"POST /foo HTTP/1.1\r\nHost: a.com\r\nTransfer-Encoding : chunked\r\n\r\n4\r\ntest\r\n0\r\n\r\n",
}
rest, err := io.ReadAll(br)
if err != nil {
t.Fatalf("unexpected read error: %v", err)
}
if got := string(rest); got != "NEXT" {
t.Fatalf("unexpected buffered bytes %q", got)
for _, s := range tests {
var req Request
br := bufio.NewReader(bytes.NewBufferString(s))
if err := req.ReadLimitBody(br, 10); err == nil {
t.Fatalf("expecting error for %q", s)
}
if body := req.Body(); len(body) != 0 {
t.Fatalf("unexpected body %q for %q", body, s)
}
}
}