Nicholas Husin would like Damien Neil to review this change.
internal/http3: validate pseudo-header counts in server
Following RFC 9114, CONNECT requests must have exactly one value for
:method, :scheme, and :path pseudo-headers. CONNECT requests, on the
other hand, should omit :scheme and :path, with only one :authority
pseudo-header.
For golang/go#70914
diff --git a/internal/http3/server.go b/internal/http3/server.go
index 57f6a34..aaf1e70 100644
--- a/internal/http3/server.go
+++ b/internal/http3/server.go
@@ -392,12 +392,16 @@
return nil, pseudoHeader{}, &streamError{errH3MessageError, "received other frames when expecting HEADERS"}
}
header := make(http.Header)
+ pHeaderCount := make(map[string]int)
var pHeader pseudoHeader
var dec qpackDecoder
if err := dec.decode(st, func(_ indexType, name, value string) error {
if !httpguts.ValidHeaderFieldValue(value) {
return &streamError{errH3MessageError, "invalid field value"}
}
+ if strings.HasPrefix(name, ":") {
+ pHeaderCount[name]++
+ }
switch name {
case ":method":
pHeader.method = value
@@ -423,6 +427,32 @@
if hasDisallowedConnectionHeader(header) {
return nil, pseudoHeader{}, &streamError{errH3MessageError, "invalid connection-related header"}
}
+
+ // "All HTTP/3 requests MUST include exactly one value for the :method,
+ // :scheme, and :path pseudo-header fields, unless the request is a CONNECT
+ // request"
+ //
+ // "A CONNECT request MUST be constructed as follows:
+ // - The :method pseudo-header field is set to "CONNECT"
+ // - The :scheme and :path pseudo-header fields are omitted
+ // - The :authority pseudo-header field contains the host and port to connect to"
+ invalidPHeaderErr := &streamError{errH3MessageError, "invalid pseudo-header"}
+ methodCount := pHeaderCount[":method"]
+ schemeCount := pHeaderCount[":scheme"]
+ pathCount := pHeaderCount[":path"]
+ authCount := pHeaderCount[":authority"]
+ if methodCount != 1 {
+ return nil, pseudoHeader{}, invalidPHeaderErr
+ }
+ if pHeader.method != "CONNECT" {
+ if schemeCount != 1 || pathCount != 1 {
+ return nil, pseudoHeader{}, invalidPHeaderErr
+ }
+ } else {
+ if schemeCount != 0 || pathCount != 0 || authCount != 1 {
+ return nil, pseudoHeader{}, invalidPHeaderErr
+ }
+ }
return header, pHeader, nil
}
diff --git a/internal/http3/server_test.go b/internal/http3/server_test.go
index eb1989a..acb8025 100644
--- a/internal/http3/server_test.go
+++ b/internal/http3/server_test.go
@@ -235,6 +235,95 @@
})
}
+func TestServerPseudoHeaderCount(t *testing.T) {
+ tests := []struct {
+ name string
+ header http.Header
+ wantError bool
+ }{
+ {
+ name: "missing method pseudo-header",
+ header: http.Header{
+ ":scheme": {"https"},
+ ":path": {"/"},
+ ":authority": {"fake.tld:1234"},
+ },
+ wantError: true,
+ },
+ {
+ name: "valid pseudo-headers for non-CONNECT request",
+ header: http.Header{
+ ":method": {"GET"},
+ ":scheme": {"https"},
+ ":path": {"/"},
+ },
+ wantError: false,
+ },
+ {
+ name: "extraneous pseudo-headers for non-CONNECT request",
+ header: http.Header{
+ ":method": {"GET", "GET"}, // Duplicate :method.
+ ":scheme": {"https"},
+ ":path": {"/"},
+ },
+ wantError: true,
+ },
+ {
+ name: "missing pseudo-headers for non-CONNECT request",
+ header: http.Header{
+ ":method": {"GET", "GET"},
+ ":path": {"/"},
+ },
+ wantError: true,
+ },
+ {
+ name: "valid pseudo-headers for CONNECT request",
+ header: http.Header{
+ ":method": {"CONNECT"},
+ ":authority": {"fake.tld:1234"},
+ },
+ wantError: false,
+ },
+ {
+ name: "extraneous pseudo-headers for CONNECT request",
+ header: http.Header{
+ ":method": {"CONNECT"},
+ ":authority": {"fake.tld:1234"},
+ ":path": {"/"}, // :path should be omitted.
+ },
+ wantError: true,
+ },
+ {
+ name: "missing pseudo-headers for CONNECT request",
+ header: http.Header{
+ ":method": {"CONNECT"},
+ },
+ wantError: true,
+ },
+ }
+ for _, tt := range tests {
+ synctestSubtest(t, tt.name, func(t *testing.T) {
+ body := []byte("some data")
+ ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write(body)
+ }))
+ tc := ts.connect()
+ tc.greet()
+
+ reqStream := tc.newStream(streamTypeRequest)
+ reqStream.writeHeaders(tt.header)
+
+ if tt.wantError {
+ reqStream.wantError(quic.StreamErrorCode(errH3MessageError))
+ } else {
+ reqStream.wantHeaders(nil)
+ reqStream.wantData(body)
+ reqStream.wantClosed("request is complete")
+ }
+ })
+ }
+}
+
func TestServerInvalidHeader(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
pHeaderCount := make(map[string]int)Let's not create a transient map on each request. I'm not sure if escape analysis avoids putting this on the heap or not, but it's probably not very efficient. And we don't care about the number of headers, just whether each is present.
It might be a little bit more code, but a collection of bools should be efficient: `hasMethod`, `hasScheme`, etc.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Let's not create a transient map on each request. I'm not sure if escape analysis avoids putting this on the heap or not, but it's probably not very efficient. And we don't care about the number of headers, just whether each is present.
It might be a little bit more code, but a collection of bools should be efficient: `hasMethod`, `hasScheme`, etc.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |