From 048038fb359515f385bf24e25a6bafe91af1d3db Mon Sep 17 00:00:00 2001 From: Nayyar Date: Thu, 25 Jun 2026 12:37:17 +0530 Subject: [PATCH] reject empty segments in jwt validators --- .../fwvalidators/framework_validators.go | 12 ++- .../fwvalidators/framework_validators_test.go | 94 +++++++++++++++++++ .../provider/provider_internal_test.go | 14 +++ .../terraform/provider/provider_validators.go | 8 +- 4 files changed, 126 insertions(+), 2 deletions(-) diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators.go b/mmv1/third_party/terraform/fwvalidators/framework_validators.go index eed3a32e98f9..1e852a5030f0 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators.go @@ -249,8 +249,18 @@ func (v jwtValidator) ValidateString(ctx context.Context, request validator.Stri return } - // Check that each part is base64 encoded + // Check that each part is non-empty and base64url encoded. An empty + // segment decodes without error, so a token like ".." would otherwise + // be accepted as a valid JWT. for i, part := range parts { + if part == "" { + response.Diagnostics.AddAttributeError( + request.Path, + "Invalid JWT Format", + fmt.Sprintf("Part %d of JWT must not be empty (expected header.payload.signature)", i+1), + ) + continue + } if _, err := base64.RawURLEncoding.DecodeString(part); err != nil { response.Diagnostics.AddAttributeError( request.Path, diff --git a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go index 8d8a285584fd..10dea9678c4d 100644 --- a/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go +++ b/mmv1/third_party/terraform/fwvalidators/framework_validators_test.go @@ -2,6 +2,7 @@ package fwvalidators_test import ( "context" + "strings" "testing" "time" @@ -161,6 +162,99 @@ func TestServiceAccountEmailValidator(t *testing.T) { } } +func TestJWTValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + value types.String + expectError bool + errorContains string + } + + tests := map[string]testCase{ + "valid jwt": { + value: types.StringValue("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjMifQ.c2ln"), + expectError: false, + }, + "empty string": { + value: types.StringValue(""), + expectError: true, + errorContains: "JWT token must not be empty", + }, + "wrong number of segments": { + value: types.StringValue("header.payload"), + expectError: true, + errorContains: "JWT token must have 3 parts separated by dots (header.payload.signature)", + }, + "all segments empty": { + value: types.StringValue(".."), + expectError: true, + errorContains: "Part 1 of JWT must not be empty (expected header.payload.signature)", + }, + "empty payload segment": { + value: types.StringValue("ab..cd"), + expectError: true, + errorContains: "Part 2 of JWT must not be empty (expected header.payload.signature)", + }, + "empty signature segment": { + value: types.StringValue("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjMifQ."), + expectError: true, + errorContains: "Part 3 of JWT must not be empty (expected header.payload.signature)", + }, + "invalid base64 segment": { + value: types.StringValue("not base64.eyJzdWIiOiIxMjMifQ.c2ln"), + expectError: true, + errorContains: "Part 1 of JWT is not valid base64", + }, + "null value": { + value: types.StringNull(), + expectError: false, + }, + "unknown value": { + value: types.StringUnknown(), + expectError: false, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.value, + } + response := validator.StringResponse{} + v := fwvalidators.JWTValidator() + + v.ValidateString(context.Background(), request, &response) + + if test.expectError && !response.Diagnostics.HasError() { + t.Errorf("expected error, got none for value: %q", test.value.ValueString()) + } + + if !test.expectError && response.Diagnostics.HasError() { + t.Errorf("got unexpected error for value: %q: %s", test.value.ValueString(), response.Diagnostics.Errors()) + } + + if test.errorContains != "" { + foundError := false + for _, err := range response.Diagnostics.Errors() { + if strings.Contains(err.Detail(), test.errorContains) { + foundError = true + break + } + } + if !foundError { + t.Errorf("expected error containing %q, got %v", test.errorContains, response.Diagnostics.Errors()) + } + } + }) + } +} + func TestBoundedDuration(t *testing.T) { t.Parallel() diff --git a/mmv1/third_party/terraform/provider/provider_internal_test.go b/mmv1/third_party/terraform/provider/provider_internal_test.go index 03a02a4d808a..ae1d3d5b57b5 100644 --- a/mmv1/third_party/terraform/provider/provider_internal_test.go +++ b/mmv1/third_party/terraform/provider/provider_internal_test.go @@ -113,6 +113,20 @@ func TestProvider_ValidateJWT(t *testing.T) { errors.New("\"\" is not a valid JWT format"), }, }, + "a JWT with an empty segment is rejected": { + ConfigValue: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..c2ln", + ExpectedErrors: []error{ + errors.New("part 2 of JWT must not be empty (expected header.payload.signature)"), + }, + }, + "a JWT with all empty segments is rejected": { + ConfigValue: "..", + ExpectedErrors: []error{ + errors.New("part 1 of JWT must not be empty (expected header.payload.signature)"), + errors.New("part 2 of JWT must not be empty (expected header.payload.signature)"), + errors.New("part 3 of JWT must not be empty (expected header.payload.signature)"), + }, + }, "unconfigured value is not valid": { ValueNotProvided: true, ExpectedErrors: []error{ diff --git a/mmv1/third_party/terraform/provider/provider_validators.go b/mmv1/third_party/terraform/provider/provider_validators.go index c0df520da106..973a6cb6e130 100644 --- a/mmv1/third_party/terraform/provider/provider_validators.go +++ b/mmv1/third_party/terraform/provider/provider_validators.go @@ -69,8 +69,14 @@ func ValidateJWT(v interface{}, k string) (warnings []string, errors []error) { return } - // Check that each part is base64 encoded + // Check that each part is non-empty and base64url encoded. An empty + // segment decodes without error, so a token like ".." would otherwise + // be accepted as a valid JWT. for i, part := range parts { + if part == "" { + errors = append(errors, fmt.Errorf("part %d of JWT must not be empty (expected header.payload.signature)", i+1)) + continue + } if _, err := base64.RawURLEncoding.DecodeString(part); err != nil { errors = append(errors, fmt.Errorf("part %d of JWT is not valid base64: %v", i+1, err)) }