Skip to content

Commit 93e31b4

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 9d1988d commit 93e31b4

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
@@ -2315,6 +2315,22 @@ func (s) TestHeadersHTTPStatusGRPCStatus(t *testing.T) {
23152315
grpcStatusWant: "13",
23162316
grpcMessageWant: "both must only have 1 value as per HTTP/2 spec",
23172317
},
2318+
// If neither :authority nor host header is present on a gRPC request, the
2319+
// request should be rejected with HTTP Status 400 and gRPC status Internal.
2320+
{
2321+
name: "Missing authority and host header grpc",
2322+
headers: []struct {
2323+
name string
2324+
values []string
2325+
}{
2326+
{name: ":method", values: []string{"POST"}},
2327+
{name: ":path", values: []string{"foo"}},
2328+
{name: "content-type", values: []string{"application/grpc"}},
2329+
},
2330+
httpStatusWant: "400",
2331+
grpcStatusWant: "13",
2332+
grpcMessageWant: "no host or :authority header present",
2333+
},
23182334
// If the client sends an HTTP/2 request with a :method header with a
23192335
// value other than POST, as specified in the gRPC over HTTP/2
23202336
// 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
@@ -61,11 +61,14 @@ func RouteAndProcess(ctx context.Context) error {
6161
if !ok {
6262
return errors.New("missing metadata in incoming context")
6363
}
64-
// A41 added logic to the core grpc implementation to guarantee that once
65-
// the RPC gets to this point, there will be a single, unambiguous authority
66-
// present in the header map.
64+
// A41 added logic to the core grpc implementation to guarantee that once the
65+
// RPC gets to this point, there will be a single, unambiguous authority
66+
// present in the header map. But add a defensive check to ensure authority
67+
// header is present.
6768
authority := md.Get(":authority")
68-
// authority[0] is safe because of the guarantee mentioned above.
69+
if len(authority) == 0 {
70+
return rc.statusErrWithNodeID(codes.Internal, "no :authority header present")
71+
}
6972
vh := findBestMatchingVirtualHostServer(authority[0], rc.vhs)
7073
if vh == nil {
7174
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",
@@ -6746,18 +6747,6 @@ func (s) TestAuthorityHeader(t *testing.T) {
67466747
},
67476748
wantAuthority: "localhost",
67486749
},
6749-
{
6750-
name: "Missing :authority and host",
6751-
// Codepath triggered by incoming headers with no :authority and no
6752-
// host.
6753-
headers: []string{
6754-
":method", "POST",
6755-
":path", "/grpc.testing.TestService/UnaryCall",
6756-
"content-type", "application/grpc",
6757-
"te", "trailers",
6758-
},
6759-
wantAuthority: "",
6760-
},
67616750
// "If :authority is present, Host must be discarded." - A41
67626751
{
67636752
name: ":authority and host present",
@@ -6820,6 +6809,73 @@ func (s) TestAuthorityHeader(t *testing.T) {
68206809
}
68216810
}
68226811

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

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)