Skip to content

Commit cf612c8

Browse files
Fix bug with non-ASCII HTTP headers (#3197)
* Fix bug with non-ASCII HTTP headers gRPC metadata (for the non-binary keys) must be printable ASCII, i.e. each byte must be between 0x20 and 0x7E inclusive. * fix bug * more precise log messages * address comments * fix bug and add test * test for invalid header name * generated change to BUILD file
1 parent 7a1ed2b commit cf612c8

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed

examples/internal/integration/integration_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func TestEcho(t *testing.T) {
5353
testEchoBody(t, 8089, apiPrefix, true)
5454
testEchoBody(t, 8089, apiPrefix, false)
5555
testEchoBodyParamOverwrite(t, 8088)
56+
testEchoWithNonASCIIHeaderValues(t, 8088, apiPrefix)
57+
testEchoWithInvalidHeaderKey(t, 8088, apiPrefix)
5658
})
5759
}
5860
}
@@ -2504,3 +2506,79 @@ func testABETrace(t *testing.T, port int) {
25042506
return
25052507
}
25062508
}
2509+
2510+
func testEchoWithNonASCIIHeaderValues(t *testing.T, port int, apiPrefix string) {
2511+
apiURL := fmt.Sprintf("http://localhost:%d/%s/example/echo/myid", port, apiPrefix)
2512+
2513+
req, err := http.NewRequest("POST", apiURL, strings.NewReader("{}"))
2514+
if err != nil {
2515+
t.Errorf("http.NewRequest() = err: %v", err)
2516+
return
2517+
}
2518+
req.Header.Add("Content-Type", "application/json")
2519+
req.Header.Add("Grpc-Metadata-Location", "Gjøvik")
2520+
resp, err := http.DefaultClient.Do(req)
2521+
if err != nil {
2522+
t.Errorf("http.Post(%q) failed with %v; want success", apiURL, err)
2523+
return
2524+
}
2525+
defer resp.Body.Close()
2526+
2527+
buf, err := io.ReadAll(resp.Body)
2528+
if err != nil {
2529+
t.Errorf("io.ReadAll(resp.Body) failed with %v; want success", err)
2530+
return
2531+
}
2532+
2533+
if got, want := resp.StatusCode, http.StatusOK; got != want {
2534+
t.Errorf("resp.StatusCode = %d; want %d", got, want)
2535+
t.Logf("%s", buf)
2536+
}
2537+
2538+
msg := new(examplepb.UnannotatedSimpleMessage)
2539+
if err := marshaler.Unmarshal(buf, msg); err != nil {
2540+
t.Errorf("marshaler.Unmarshal(%s, msg) failed with %v; want success", buf, err)
2541+
return
2542+
}
2543+
if got, want := msg.Id, "myid"; got != want {
2544+
t.Errorf("msg.Id = %q; want %q", got, want)
2545+
}
2546+
}
2547+
2548+
func testEchoWithInvalidHeaderKey(t *testing.T, port int, apiPrefix string) {
2549+
apiURL := fmt.Sprintf("http://localhost:%d/%s/example/echo/myid", port, apiPrefix)
2550+
2551+
req, err := http.NewRequest("POST", apiURL, strings.NewReader("{}"))
2552+
if err != nil {
2553+
t.Errorf("http.NewRequest() = err: %v", err)
2554+
return
2555+
}
2556+
req.Header.Add("Content-Type", "application/json")
2557+
req.Header.Add("Grpc-Metadata-Foo+Bar", "Hello")
2558+
resp, err := http.DefaultClient.Do(req)
2559+
if err != nil {
2560+
t.Errorf("http.Post(%q) failed with %v; want success", apiURL, err)
2561+
return
2562+
}
2563+
defer resp.Body.Close()
2564+
2565+
buf, err := io.ReadAll(resp.Body)
2566+
if err != nil {
2567+
t.Errorf("io.ReadAll(resp.Body) failed with %v; want success", err)
2568+
return
2569+
}
2570+
2571+
if got, want := resp.StatusCode, http.StatusOK; got != want {
2572+
t.Errorf("resp.StatusCode = %d; want %d", got, want)
2573+
t.Logf("%s", buf)
2574+
}
2575+
2576+
msg := new(examplepb.UnannotatedSimpleMessage)
2577+
if err := marshaler.Unmarshal(buf, msg); err != nil {
2578+
t.Errorf("marshaler.Unmarshal(%s, msg) failed with %v; want success", buf, err)
2579+
return
2580+
}
2581+
if got, want := msg.Id, "myid"; got != want {
2582+
t.Errorf("msg.Id = %q; want %q", got, want)
2583+
}
2584+
}

runtime/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ go_library(
2626
deps = [
2727
"//internal/httprule",
2828
"//utilities",
29+
"@com_github_golang_glog//:glog",
2930
"@go_googleapis//google/api:httpbody_go_proto",
3031
"@io_bazel_rules_go//proto/wkt:field_mask_go_proto",
3132
"@org_golang_google_grpc//codes",

runtime/context.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sync"
1313
"time"
1414

15+
"github.com/golang/glog"
1516
"google.golang.org/grpc/codes"
1617
"google.golang.org/grpc/metadata"
1718
"google.golang.org/grpc/status"
@@ -99,6 +100,38 @@ func AnnotateIncomingContext(ctx context.Context, mux *ServeMux, req *http.Reque
99100
return metadata.NewIncomingContext(ctx, md), nil
100101
}
101102

103+
func isValidGRPCMetadataKey(key string) bool {
104+
// Must be a valid gRPC "Header-Name" as defined here:
105+
// https://github.com/grpc/grpc/blob/4b05dc88b724214d0c725c8e7442cbc7a61b1374/doc/PROTOCOL-HTTP2.md
106+
// This means 0-9 a-z _ - .
107+
// Only lowercase letters are valid in the wire protocol, but the client library will normalize
108+
// uppercase ASCII to lowercase, so uppercase ASCII is also acceptable.
109+
bytes := []byte(key) // gRPC validates strings on the byte level, not Unicode.
110+
for _, ch := range bytes {
111+
validLowercaseLetter := ch >= 'a' && ch <= 'z'
112+
validUppercaseLetter := ch >= 'A' && ch <= 'Z'
113+
validDigit := ch >= '0' && ch <= '9'
114+
validOther := ch == '.' || ch == '-' || ch == '_'
115+
if !validLowercaseLetter && !validUppercaseLetter && !validDigit && !validOther {
116+
return false
117+
}
118+
}
119+
return true
120+
}
121+
122+
func isValidGRPCMetadataTextValue(textValue string) bool {
123+
// Must be a valid gRPC "ASCII-Value" as defined here:
124+
// https://github.com/grpc/grpc/blob/4b05dc88b724214d0c725c8e7442cbc7a61b1374/doc/PROTOCOL-HTTP2.md
125+
// This means printable ASCII (including/plus spaces); 0x20 to 0x7E inclusive.
126+
bytes := []byte(textValue) // gRPC validates strings on the byte level, not Unicode.
127+
for _, ch := range bytes {
128+
if ch < 0x20 || ch > 0x7E {
129+
return false
130+
}
131+
}
132+
return true
133+
}
134+
102135
func annotateContext(ctx context.Context, mux *ServeMux, req *http.Request, rpcMethodName string, options ...AnnotateContextOption) (context.Context, metadata.MD, error) {
103136
ctx = withRPCMethod(ctx, rpcMethodName)
104137
for _, o := range options {
@@ -121,6 +154,10 @@ func annotateContext(ctx context.Context, mux *ServeMux, req *http.Request, rpcM
121154
pairs = append(pairs, "authorization", val)
122155
}
123156
if h, ok := mux.incomingHeaderMatcher(key); ok {
157+
if !isValidGRPCMetadataKey(h) {
158+
glog.Errorf("HTTP header name %q is not valid as gRPC metadata key; skipping", h)
159+
continue
160+
}
124161
// Handles "-bin" metadata in grpc, since grpc will do another base64
125162
// encode before sending to server, we need to decode it first.
126163
if strings.HasSuffix(key, metadataHeaderBinarySuffix) {
@@ -130,6 +167,9 @@ func annotateContext(ctx context.Context, mux *ServeMux, req *http.Request, rpcM
130167
}
131168

132169
val = string(b)
170+
} else if !isValidGRPCMetadataTextValue(val) {
171+
glog.Errorf("Value of HTTP header %q contains non-ASCII value (not valid as gRPC metadata): skipping", h)
172+
continue
133173
}
134174
pairs = append(pairs, h, val)
135175
}

0 commit comments

Comments
 (0)