Skip to content

Commit b940584

Browse files
transport: reject requests missing :authority and host headers (#9365)
Fixes : #9354 This PR updates the HTTP/2 transport layer to reject requests missing both :authority and Host headers early. Also adds a defensive check in xds server to ensure authority header is present. RELEASE NOTES: - server: Reject requests missing both `:authority` and `Host` headers with HTTP 400 and status `Internal` --------- Co-authored-by: Madan Kumar <winklemad@outlook.com>
1 parent 4b63829 commit b940584

6 files changed

Lines changed: 134 additions & 16 deletions

File tree

internal/transport/http2_server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,12 @@ func (t *http2Server) operateHeaders(ctx context.Context, frame *http2.MetaHeade
522522
delete(mdata, "host")
523523
}
524524

525+
// If :authority is still missing, i.e. no host or :authority header is
526+
// present, reject the request as invalid.
527+
if len(mdata[":authority"]) == 0 {
528+
t.writeEarlyAbort(streamID, s.contentSubtype, status.New(codes.Internal, "no host or :authority header present"), http.StatusBadRequest, !frame.StreamEnded())
529+
return nil
530+
}
525531
if frame.StreamEnded() {
526532
// s is just created by the caller. No lock needed.
527533
s.state = streamReadDone

internal/transport/transport_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,22 @@ func (s) TestHeadersHTTPStatusGRPCStatus(t *testing.T) {
22642264
grpcStatusWant: "13",
22652265
grpcMessageWant: "both must only have 1 value as per HTTP/2 spec",
22662266
},
2267+
// If neither :authority nor host header is present on a gRPC request, the
2268+
// request should be rejected with HTTP Status 400 and gRPC status Internal.
2269+
{
2270+
name: "Missing authority and host header grpc",
2271+
headers: []struct {
2272+
name string
2273+
values []string
2274+
}{
2275+
{name: ":method", values: []string{"POST"}},
2276+
{name: ":path", values: []string{"foo"}},
2277+
{name: "content-type", values: []string{"application/grpc"}},
2278+
},
2279+
httpStatusWant: "400",
2280+
grpcStatusWant: "13",
2281+
grpcMessageWant: "no host or :authority header present",
2282+
},
22672283
// If the client sends an HTTP/2 request with a :method header with a
22682284
// value other than POST, as specified in the gRPC over HTTP/2
22692285
// specification, the server should fail the RPC.

internal/xds/server/routing.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ func RouteAndProcess(ctx context.Context) error {
6262
if !ok {
6363
return errors.New("missing metadata in incoming context")
6464
}
65-
// A41 added logic to the core grpc implementation to guarantee that once
66-
// the RPC gets to this point, there will be a single, unambiguous authority
67-
// present in the header map.
65+
// A41 added logic to the core grpc implementation to guarantee that once the
66+
// RPC gets to this point, there will be a single, unambiguous authority
67+
// present in the header map. But add a defensive check to ensure authority
68+
// header is present.
6869
authority := md.Get(":authority")
69-
// authority[0] is safe because of the guarantee mentioned above.
70+
if len(authority) == 0 {
71+
return rc.statusErrWithNodeID(codes.Internal, "no :authority header present")
72+
}
7073
vh := findBestMatchingVirtualHostServer(authority[0], rc.vhs)
7174
if vh == nil {
7275
return rc.statusErrWithNodeID(codes.Unavailable, "the incoming RPC did not match a configured Virtual Host")

internal/xds/server/routing_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919
package server
2020

2121
import (
22+
"context"
23+
"strings"
24+
"sync/atomic"
2225
"testing"
2326

2427
"github.com/google/go-cmp/cmp"
28+
"google.golang.org/grpc"
29+
"google.golang.org/grpc/codes"
30+
"google.golang.org/grpc/internal/transport"
31+
"google.golang.org/grpc/metadata"
32+
"google.golang.org/grpc/status"
2533
)
2634

2735
func (s) TestMatchTypeForDomain(t *testing.T) {
@@ -107,3 +115,31 @@ func (s) TestFindBestMatchingVirtualHost(t *testing.T) {
107115
})
108116
}
109117
}
118+
119+
type testServerTransportStream struct {
120+
method string
121+
}
122+
123+
func (s *testServerTransportStream) Method() string { return s.method }
124+
func (s *testServerTransportStream) SetHeader(metadata.MD) error { return nil }
125+
func (s *testServerTransportStream) SendHeader(metadata.MD) error { return nil }
126+
func (s *testServerTransportStream) SetTrailer(metadata.MD) error { return nil }
127+
128+
func (s) TestRouteAndProcess_MissingAuthority(t *testing.T) {
129+
var ptr atomic.Pointer[usableRouteConfiguration]
130+
ptr.Store(&usableRouteConfiguration{})
131+
cw := &connWrapper{urc: &ptr}
132+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
133+
defer cancel()
134+
ctx = transport.SetConnection(ctx, cw)
135+
ctx = grpc.NewContextWithServerTransportStream(ctx, &testServerTransportStream{method: "/test.Service/Method"})
136+
ctx = metadata.NewIncomingContext(ctx, metadata.MD{})
137+
138+
err := RouteAndProcess(ctx)
139+
if status.Code(err) != codes.Internal {
140+
t.Fatalf("RouteAndProcess() returned error code %v, want %v", status.Code(err), codes.Internal)
141+
}
142+
if !strings.Contains(err.Error(), "no :authority header present") {
143+
t.Fatalf("RouteAndProcess() returned error message %q, want %q", err.Error(), "no :authority header present")
144+
}
145+
}

test/end2end_test.go

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4602,6 +4602,7 @@ func (s) TestZeroSecondTimeout(t *testing.T) {
46024602
BlockFragment: st.encodeHeader(
46034603
":method", "POST",
46044604
":path", "/grpc.testing.TestService/StreamingInputCall",
4605+
":authority", "localhost",
46054606
"content-type", "application/grpc",
46064607
"te", "trailers",
46074608
"grpc-timeout", "0n",
@@ -6745,18 +6746,6 @@ func (s) TestAuthorityHeader(t *testing.T) {
67456746
},
67466747
wantAuthority: "localhost",
67476748
},
6748-
{
6749-
name: "Missing :authority and host",
6750-
// Codepath triggered by incoming headers with no :authority and no
6751-
// host.
6752-
headers: []string{
6753-
":method", "POST",
6754-
":path", "/grpc.testing.TestService/UnaryCall",
6755-
"content-type", "application/grpc",
6756-
"te", "trailers",
6757-
},
6758-
wantAuthority: "",
6759-
},
67606749
// "If :authority is present, Host must be discarded." - A41
67616750
{
67626751
name: ":authority and host present",
@@ -6819,6 +6808,73 @@ func (s) TestAuthorityHeader(t *testing.T) {
68196808
}
68206809
}
68216810

6811+
// TestMissingAuthorityAndHostHeader tests that an incoming HTTP/2 request with
6812+
// neither :authority nor host header is rejected with HTTP status 400 and gRPC
6813+
// status Internal.
6814+
func (s) TestMissingAuthorityAndHostHeader(t *testing.T) {
6815+
lis, err := net.Listen("tcp", "localhost:0")
6816+
if err != nil {
6817+
t.Fatalf("Failed to listen: %v", err)
6818+
}
6819+
defer lis.Close()
6820+
s := grpc.NewServer()
6821+
defer s.Stop()
6822+
go s.Serve(lis)
6823+
6824+
conn, err := net.DialTimeout("tcp", lis.Addr().String(), defaultTestTimeout)
6825+
if err != nil {
6826+
t.Fatalf("Failed to dial server: %v", err)
6827+
}
6828+
defer conn.Close()
6829+
6830+
st := newServerTesterFromConn(t, conn)
6831+
st.greet()
6832+
6833+
st.writeHeaders(http2.HeadersFrameParam{
6834+
StreamID: 1,
6835+
BlockFragment: st.encodeHeader(
6836+
":method", "POST",
6837+
":path", "/grpc.testing.TestService/UnaryCall",
6838+
"content-type", "application/grpc",
6839+
"te", "trailers",
6840+
),
6841+
EndStream: false,
6842+
EndHeaders: true,
6843+
})
6844+
6845+
for {
6846+
frame, err := st.readFrame()
6847+
if err != nil {
6848+
t.Fatalf("Error reading frame: %v", err)
6849+
}
6850+
hf, ok := frame.(*http2.MetaHeadersFrame)
6851+
if !ok {
6852+
continue
6853+
}
6854+
var httpStatus, grpcStatus, grpcMessage string
6855+
for _, h := range hf.Fields {
6856+
switch h.Name {
6857+
case ":status":
6858+
httpStatus = h.Value
6859+
case "grpc-status":
6860+
grpcStatus = h.Value
6861+
case "grpc-message":
6862+
grpcMessage = h.Value
6863+
}
6864+
}
6865+
if httpStatus != "400" {
6866+
t.Fatalf("Got HTTP status %v, want 400", httpStatus)
6867+
}
6868+
if grpcStatus != "13" {
6869+
t.Fatalf("Got gRPC status %v, want 13 (Internal)", grpcStatus)
6870+
}
6871+
if !strings.Contains(grpcMessage, "no host or :authority header present") {
6872+
t.Fatalf("Got gRPC message %q, want 'no host or :authority header present'", grpcMessage)
6873+
}
6874+
return
6875+
}
6876+
}
6877+
68226878
func (s) TestHTTPServerSendsNonGRPCHeaderSurfaceFurtherData(t *testing.T) {
68236879
const nonGRPCDataMaxLen = 1024
68246880
tests := []struct {

test/servertester.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ func (st *serverTester) writeHeadersGRPC(streamID uint32, path string, endStream
286286
BlockFragment: st.encodeHeader(
287287
":method", "POST",
288288
":path", path,
289+
":authority", "localhost",
289290
"content-type", "application/grpc",
290291
"te", "trailers",
291292
),

0 commit comments

Comments
 (0)