diff --git a/tests/e2e/v3_curl_test.go b/tests/e2e/v3_curl_test.go index b994b48973c7..31f1f8838eae 100644 --- a/tests/e2e/v3_curl_test.go +++ b/tests/e2e/v3_curl_test.go @@ -405,3 +405,35 @@ func CURLWithExpected(cx ctlCtx, tests []v3cURLTest) error { } return nil } + +func TestV3CurlInvalidUTF8VersionHandling(t *testing.T) { + for _, p := range apiPrefix { + testCtl(t, testV3CurlInvalidUTF8VersionHandling, withApiPrefix(p), withCfg(*e2e.NewConfigNoTLS())) + } +} + +func testV3CurlInvalidUTF8VersionHandling(cx ctlCtx) { + p := cx.apiPrefix + + // Create an ordinary put payload + putData, err := json.Marshal(&pb.PutRequest{ + Key: []byte("foo"), + Value: []byte("bar"), // this will be automatically base64-encoded by Go + }) + if err != nil { + cx.t.Fatal(err) + } + + // Create a curl request with a client api version header that contains invalid utf8 string + req := e2e.CURLReq{ + Endpoint: path.Join(p, "/kv/put"), + Header: "client-api-version: hello\xff\xfe\xfdworld", + Value: string(putData), + Expected: "revision", + } + + // Post request and expect no errors as etcd should not panic due to this + if err := e2e.CURLPost(cx.epc, req); err != nil { + cx.t.Fatalf("failed (%s) (%v)", p, err) + } +} diff --git a/tests/integration/clientv3/kv_test.go b/tests/integration/clientv3/kv_test.go index 3442f5285f41..2a9cc775a456 100644 --- a/tests/integration/clientv3/kv_test.go +++ b/tests/integration/clientv3/kv_test.go @@ -27,6 +27,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "go.etcd.io/etcd/api/v3/mvccpb" @@ -896,3 +897,55 @@ func TestBalancerSupportLearner(t *testing.T) { t.Errorf("expect no error (balancer should retry when request to learner fails), got error: %v", err) } } + +func TestCtxClientVersionOverwrite(t *testing.T) { + + // Initialize integration test framework + integration2.BeforeTest(t) + + // Create a new single node cluster + clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1}) + defer clus.Terminate(t) + + // Create a client request with an invalid client api version + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = metadata.NewIncomingContext(ctx, metadata.New(map[string]string{rpctypes.MetadataClientAPIVersionKey: "hello\xff\xfe\xfdworld"})) + _, err := clus.Members[0].Client.Put(ctx, "/foo", "bar") + + // We should see no error because downstream call stack for clientv3 + // should overwrite the invalid version created in the ctx here as part of + // `client/v3/ctx.go:func withVersion` + if err != nil { + t.Fatalf("Expected no error (clientv3 should be overwriting clientapiversion): %v", err) + } +} + +func TestInvalidUTF8ClientVersionRejection(t *testing.T) { + + // Initialize integration test framework + integration2.BeforeTest(t) + + // Create a new single node cluster + clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1}) + defer clus.Terminate(t) + + // Set an invalid API version for the client to rely on + invalidVersion := []byte{0x01, 0x02, 0x03} + version.APIVersion = string(invalidVersion) + + // Create a client request with an invalid client api version + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _, err := clus.Members[0].Client.Put(ctx, "/foo", "bar") + defer cancel() + + // We should see a grpc internal error because downstream call stack for clientv3 + // should reject any headers that contain non printable characters. + // Refer grpc stream.go call to imetadata.Validate(md) + if err == nil { + t.Fatalf("A client with non printable characters %v in api version should create an error", invalidVersion) + } + if err != nil { + t.Logf("Error returned is: %v", err) + } +}