CVE-2024-2689
Description
Denial of Service in Temporal Server prior to version 1.20.5, 1.21.6, and 1.22.7 allows an authenticated user who has permissions to interact with workflows and has crafted an invalid UTF-8 string for submission to potentially cause a crashloop. If left unchecked, the task containing the invalid UTF-8 will become stuck in the queue, causing an increase in queue lag. Eventually, all processes handling these queues will become stuck and the system will run out of resources. The workflow ID of the failing task will be visible in the logs, and can be used to remove that workflow as a mitigation. Version 1.23 is not impacted. In this context, a user is an operator of Temporal Server.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/temporalio/temporalGo | >= 1.22.0-rc1, < 1.22.7 | 1.22.7 |
github.com/temporalio/temporalGo | >= 1.21.0, < 1.21.6 | 1.21.6 |
github.com/temporalio/temporalGo | < 1.20.5 | 1.20.5 |
Affected products
1Patches
3f1fab97129f9Add validation for a few string fields (#5487)
6 files changed · +170 −25
common/searchattribute/encode_value.go+28 −3 modified@@ -25,15 +25,19 @@ package searchattribute import ( + "errors" "fmt" "time" + "unicode/utf8" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/common/payload" ) +var ErrInvalidString = errors.New("SearchAttribute value is not a valid UTF-8 string") + // EncodeValue encodes search attribute value and IndexedValueType to Payload. func EncodeValue(val interface{}, t enumspb.IndexedValueType) (*commonpb.Payload, error) { valPayload, err := payload.Encode(val) @@ -70,16 +74,37 @@ func DecodeValue( case enumspb.INDEXED_VALUE_TYPE_INT: return decodeValueTyped[int64](value, allowList) case enumspb.INDEXED_VALUE_TYPE_KEYWORD: - return decodeValueTyped[string](value, allowList) + return validateStrings(decodeValueTyped[string](value, allowList)) case enumspb.INDEXED_VALUE_TYPE_TEXT: - return decodeValueTyped[string](value, allowList) + return validateStrings(decodeValueTyped[string](value, allowList)) case enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST: - return decodeValueTyped[[]string](value, false) + return validateStrings(decodeValueTyped[[]string](value, false)) default: return nil, fmt.Errorf("%w: %v", ErrInvalidType, t) } } +func validateStrings(anyValue any, err error) (any, error) { + if err != nil { + return anyValue, err + } + + // validate strings + switch value := anyValue.(type) { + case string: + if !utf8.ValidString(value) { + return nil, fmt.Errorf("%w: %s", ErrInvalidString, value) + } + case []string: + for _, item := range value { + if !utf8.ValidString(item) { + return nil, fmt.Errorf("%w: %s", ErrInvalidString, item) + } + } + } + return anyValue, err +} + // decodeValueTyped tries to decode to the given type. // If the input is a list and allowList is false, then it will return only the first element. // If the input is a list and allowList is true, then it will return the decoded list.
common/searchattribute/encode_value_test.go+19 −0 modified@@ -25,6 +25,7 @@ package searchattribute import ( + "errors" "testing" "time" @@ -372,3 +373,21 @@ func Test_EncodeValue(t *testing.T) { "Datetime Search Attribute is expected to be encoded in RFC 3339 format") s.Equal("Datetime", string(encodedPayload.Metadata["type"])) } + +func Test_ValidateStrings(t *testing.T) { + _, err := validateStrings("anything here", errors.New("test error")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "test error") + + _, err = validateStrings("\x87\x01", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid UTF-8 string") + + value, err := validateStrings("anything here", nil) + assert.Nil(t, err) + assert.Equal(t, "anything here", value) + + _, err = validateStrings([]string{"abc", "\x87\x01"}, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid UTF-8 string") +}
common/util.go+8 −0 modified@@ -32,6 +32,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/dgryski/go-farm" "github.com/gogo/protobuf/proto" @@ -720,3 +721,10 @@ func OverrideWorkflowTaskTimeout( func CloneProto[T proto.Message](v T) T { return proto.Clone(v).(T) } + +func ValidateUTF8String(fieldName string, strValue string) error { + if !utf8.ValidString(strValue) { + return serviceerror.NewInvalidArgument(fmt.Sprintf("%s %v is not a valid UTF-8 string", fieldName, strValue)) + } + return nil +}
service/frontend/workflow_handler.go+56 −21 modified@@ -380,6 +380,9 @@ func (wh *WorkflowHandler) StartWorkflowExecution(ctx context.Context, request * return nil, errWorkflowTypeTooLong } + if err := common.ValidateUTF8String("WorkflowType", request.WorkflowType.GetName()); err != nil { + return nil, err + } if err := wh.validateTaskQueue(request.TaskQueue); err != nil { return nil, err } @@ -388,12 +391,12 @@ func (wh *WorkflowHandler) StartWorkflowExecution(ctx context.Context, request * return nil, err } - if request.GetRequestId() == "" { + if request.RequestId == "" { return nil, errRequestIDNotSet } - if len(request.GetRequestId()) > wh.config.MaxIDLengthLimit() { - return nil, errRequestIDTooLong + if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil { + return nil, err } request, err := wh.unaliasStartWorkflowExecutionRequestSearchAttributes(request, namespaceName) @@ -2002,12 +2005,16 @@ func (wh *WorkflowHandler) SignalWithStartWorkflowExecution(ctx context.Context, return nil, errWorkflowTypeTooLong } + if err := common.ValidateUTF8String("WorkflowType", request.WorkflowType.GetName()); err != nil { + return nil, err + } + if err := wh.validateTaskQueue(request.TaskQueue); err != nil { return nil, err } - if len(request.GetRequestId()) > wh.config.MaxIDLengthLimit() { - return nil, errRequestIDTooLong + if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil { + return nil, err } if err := wh.validateSignalWithStartWorkflowTimeouts(request); err != nil { @@ -4348,6 +4355,9 @@ func (wh *WorkflowHandler) validateTaskQueue(t *taskqueuepb.TaskQueue) error { if len(t.GetName()) > wh.config.MaxIDLengthLimit() { return errTaskQueueTooLong } + if err := common.ValidateUTF8String("TaskQueue", t.GetName()); err != nil { + return err + } enums.SetDefaultTaskQueueKind(&t.Kind) return nil @@ -4356,26 +4366,33 @@ func (wh *WorkflowHandler) validateTaskQueue(t *taskqueuepb.TaskQueue) error { func (wh *WorkflowHandler) validateBuildIdOrderingUpdate( req *workflowservice.UpdateWorkerBuildIdOrderingRequest, ) error { - errstr := "request to update worker build id ordering requires:" - hadErr := false + errDeets := []string{"request to update worker build id compatability requires: "} + + checkIdLen := func(id string) { + if len(id) > wh.config.WorkerBuildIdSizeLimit() { + errDeets = append(errDeets, fmt.Sprintf(" Worker build IDs to be no larger than %v characters", + wh.config.WorkerBuildIdSizeLimit())) + } + + if err := common.ValidateUTF8String("BuildId", id); err != nil { + errDeets = append(errDeets, err.Error()) + } + } + if req.GetNamespace() == "" { - errstr += " `namespace` to be set" - hadErr = true + errDeets = append(errDeets, "`namespace` to be set") } if req.GetTaskQueue() == "" { - errstr += " `task_queue` to be set" - hadErr = true + errDeets = append(errDeets, "`task_queue` to be set") } if req.GetVersionId().GetWorkerBuildId() == "" { - errstr += " targeting a valid version identifier" - hadErr = true - } - if len(req.GetVersionId().GetWorkerBuildId()) > wh.config.WorkerBuildIdSizeLimit() { - errstr += fmt.Sprintf(" Worker build IDs to be no larger than %v characters", wh.config.WorkerBuildIdSizeLimit()) - hadErr = true + errDeets = append(errDeets, "targeting a valid version identifier") + } else { + checkIdLen(req.GetVersionId().GetWorkerBuildId()) } - if hadErr { - return serviceerror.NewInvalidArgument(errstr) + + if len(errDeets) > 1 { + return serviceerror.NewInvalidArgument(strings.Join(errDeets, ", ")) } return nil } @@ -4657,6 +4674,24 @@ func (wh *WorkflowHandler) validateRetryPolicy(namespaceName namespace.Name, ret return common.ValidateRetryPolicy(retryPolicy) } +func validateRequestId(requestID *string, lenLimit int) error { + if requestID == nil { + // should never happen, but just in case. + return serviceerror.NewInvalidArgument("RequestId is nil") + } + if *requestID == "" { + // For easy direct API use, we default the request ID here but expect all + // SDKs and other auto-retrying clients to set it + *requestID = uuid.New() + } + + if len(*requestID) > lenLimit { + return errRequestIDTooLong + } + + return common.ValidateUTF8String("RequestId", *requestID) +} + func (wh *WorkflowHandler) validateStartWorkflowTimeouts( request *workflowservice.StartWorkflowExecutionRequest, ) error { @@ -4753,7 +4788,7 @@ func (wh *WorkflowHandler) makeFakeContinuedAsNewEvent( func (wh *WorkflowHandler) validateNamespace( namespace string, ) error { - if err := wh.validateUTF8String(namespace); err != nil { + if err := common.ValidateUTF8String("Namespace", namespace); err != nil { return err } if len(namespace) > wh.config.MaxIDLengthLimit() { @@ -4768,7 +4803,7 @@ func (wh *WorkflowHandler) validateWorkflowID( if workflowID == "" { return errWorkflowIDNotSet } - if err := wh.validateUTF8String(workflowID); err != nil { + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { return err } if len(workflowID) > wh.config.MaxIDLengthLimit() {
service/frontend/workflow_handler_test.go+12 −0 modified@@ -2655,6 +2655,18 @@ func TestContextNearDeadline(t *testing.T) { assert.False(t, contextNearDeadline(ctx, time.Millisecond)) } +func TestValidateRequestId(t *testing.T) { + req := workflowservice.StartWorkflowExecutionRequest{RequestId: ""} + err := validateRequestId(&req.RequestId, 100) + assert.Nil(t, err) + assert.Len(t, req.RequestId, 36) // new UUID length + + req.RequestId = "\x87\x01" + err = validateRequestId(&req.RequestId, 100) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not a valid UTF-8 string") +} + func (s *workflowHandlerSuite) Test_DeleteWorkflowExecution() { config := s.newConfig() wh := s.getWorkflowHandler(config)
service/history/commandChecker.go+47 −1 modified@@ -380,6 +380,9 @@ func (v *commandAttrValidator) validateTimerScheduleAttributes( if len(attributes.GetTimerId()) > v.maxIDLengthLimit { return failedCause, serviceerror.NewInvalidArgument("TimerId exceeds length limit.") } + if err := common.ValidateUTF8String("TimerId", attributes.TimerId); err != nil { + return failedCause, err + } if timestamp.DurationValue(attributes.GetStartToFireTimeout()) <= 0 { return failedCause, serviceerror.NewInvalidArgument("A valid StartToFireTimeout is not set on command.") } @@ -498,7 +501,22 @@ func (v *commandAttrValidator) validateCancelExternalWorkflowExecutionAttributes if len(attributes.GetWorkflowId()) > v.maxIDLengthLimit { return failedCause, serviceerror.NewInvalidArgument("WorkflowId exceeds length limit.") } - runID := attributes.GetRunId() + runID := attributes.RunId + workflowID := attributes.WorkflowId + ns := attributes.Namespace + + if workflowID == "" { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId is not set on RequestCancelExternalWorkflowExecutionCommand. Namespace=%s RunId=%s", ns, runID)) + } + if len(ns) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("Namespace on RequestCancelExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s RunId=%s Namespace=%s Length=%d Limit=%d", workflowID, runID, ns, len(ns), v.maxIDLengthLimit)) + } + if len(workflowID) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId on RequestCancelExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Length=%d Limit=%d RunId=%s Namespace=%s", workflowID, len(workflowID), v.maxIDLengthLimit, runID, ns)) + } + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { + return failedCause, err + } if runID != "" && uuid.Parse(runID) == nil { return failedCause, serviceerror.NewInvalidArgument("Invalid RunId set on command.") } @@ -540,6 +558,22 @@ func (v *commandAttrValidator) validateSignalExternalWorkflowExecutionAttributes } targetRunID := attributes.Execution.GetRunId() + signalName := attributes.SignalName + workflowID := attributes.Execution.WorkflowId + ns := attributes.Namespace + + if workflowID == "" { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId is not set on SignalExternalWorkflowExecutionCommand. Namespace=%s RunId=%s SignalName=%s", ns, targetRunID, signalName)) + } + if len(ns) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("Namespace on SignalExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Namespace=%s Length=%d Limit=%d RunId=%s SignalName=%s", workflowID, ns, len(ns), v.maxIDLengthLimit, targetRunID, signalName)) + } + if len(workflowID) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId on SignalExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Length=%d Limit=%d Namespace=%s RunId=%s SignalName=%s", workflowID, len(workflowID), v.maxIDLengthLimit, ns, targetRunID, signalName)) + } + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { + return failedCause, err + } if targetRunID != "" && uuid.Parse(targetRunID) == nil { return failedCause, serviceerror.NewInvalidArgument("Invalid RunId set on command.") } @@ -791,6 +825,14 @@ func (v *commandAttrValidator) validateStartChildExecutionAttributes( return failedCause, serviceerror.NewInvalidArgument("WorkflowType exceeds length limit.") } + if err := common.ValidateUTF8String("WorkflowId", attributes.WorkflowId); err != nil { + return failedCause, err + } + + if err := common.ValidateUTF8String("WorkflowType", attributes.WorkflowType.Name); err != nil { + return failedCause, err + } + if timestamp.DurationValue(attributes.GetWorkflowExecutionTimeout()) < 0 { return failedCause, serviceerror.NewInvalidArgument("Invalid WorkflowExecutionTimeout.") } @@ -868,6 +910,10 @@ func (v *commandAttrValidator) validateTaskQueue( return taskQueue, serviceerror.NewInvalidArgument(fmt.Sprintf("task queue name exceeds length limit of %v", v.maxIDLengthLimit)) } + if err := common.ValidateUTF8String("TaskQueue", name); err != nil { + return taskQueue, err + } + if strings.HasPrefix(name, reservedTaskQueuePrefix) { return taskQueue, serviceerror.NewInvalidArgument(fmt.Sprintf("task queue name cannot start with reserved prefix %v", reservedTaskQueuePrefix)) }
679e3dc2ca8bAdd validation for a few string fields (#5487)
6 files changed · +151 −10
common/searchattribute/encode_value.go+28 −3 modified@@ -25,15 +25,19 @@ package searchattribute import ( + "errors" "fmt" "time" + "unicode/utf8" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/common/payload" ) +var ErrInvalidString = errors.New("SearchAttribute value is not a valid UTF-8 string") + // EncodeValue encodes search attribute value and IndexedValueType to Payload. func EncodeValue(val interface{}, t enumspb.IndexedValueType) (*commonpb.Payload, error) { valPayload, err := payload.Encode(val) @@ -70,16 +74,37 @@ func DecodeValue( case enumspb.INDEXED_VALUE_TYPE_INT: return decodeValueTyped[int64](value, allowList) case enumspb.INDEXED_VALUE_TYPE_KEYWORD: - return decodeValueTyped[string](value, allowList) + return validateStrings(decodeValueTyped[string](value, allowList)) case enumspb.INDEXED_VALUE_TYPE_TEXT: - return decodeValueTyped[string](value, allowList) + return validateStrings(decodeValueTyped[string](value, allowList)) case enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST: - return decodeValueTyped[[]string](value, false) + return validateStrings(decodeValueTyped[[]string](value, false)) default: return nil, fmt.Errorf("%w: %v", ErrInvalidType, t) } } +func validateStrings(anyValue any, err error) (any, error) { + if err != nil { + return anyValue, err + } + + // validate strings + switch value := anyValue.(type) { + case string: + if !utf8.ValidString(value) { + return nil, fmt.Errorf("%w: %s", ErrInvalidString, value) + } + case []string: + for _, item := range value { + if !utf8.ValidString(item) { + return nil, fmt.Errorf("%w: %s", ErrInvalidString, item) + } + } + } + return anyValue, err +} + // decodeValueTyped tries to decode to the given type. // If the input is a list and allowList is false, then it will return only the first element. // If the input is a list and allowList is true, then it will return the decoded list.
common/searchattribute/encode_value_test.go+19 −0 modified@@ -25,6 +25,7 @@ package searchattribute import ( + "errors" "testing" "time" @@ -372,3 +373,21 @@ func Test_EncodeValue(t *testing.T) { "Datetime Search Attribute is expected to be encoded in RFC 3339 format") s.Equal("Datetime", string(encodedPayload.Metadata["type"])) } + +func Test_ValidateStrings(t *testing.T) { + _, err := validateStrings("anything here", errors.New("test error")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "test error") + + _, err = validateStrings("\x87\x01", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid UTF-8 string") + + value, err := validateStrings("anything here", nil) + assert.Nil(t, err) + assert.Equal(t, "anything here", value) + + _, err = validateStrings([]string{"abc", "\x87\x01"}, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid UTF-8 string") +}
common/util.go+8 −0 modified@@ -32,6 +32,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/dgryski/go-farm" "github.com/gogo/protobuf/proto" @@ -835,3 +836,10 @@ func MakeVersionDirectiveForActivityTask( func CloneProto[T proto.Message](v T) T { return proto.Clone(v).(T) } + +func ValidateUTF8String(fieldName string, strValue string) error { + if !utf8.ValidString(strValue) { + return serviceerror.NewInvalidArgument(fmt.Sprintf("%s %v is not a valid UTF-8 string", fieldName, strValue)) + } + return nil +}
service/frontend/workflow_handler.go+37 −6 modified@@ -359,6 +359,9 @@ func (wh *WorkflowHandler) StartWorkflowExecution(ctx context.Context, request * return nil, errWorkflowTypeTooLong } + if err := common.ValidateUTF8String("WorkflowType", request.WorkflowType.GetName()); err != nil { + return nil, err + } if err := wh.validateTaskQueue(request.TaskQueue); err != nil { return nil, err } @@ -371,8 +374,8 @@ func (wh *WorkflowHandler) StartWorkflowExecution(ctx context.Context, request * return nil, errRequestIDNotSet } - if len(request.GetRequestId()) > wh.config.MaxIDLengthLimit() { - return nil, errRequestIDTooLong + if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil { + return nil, err } request, err := wh.unaliasStartWorkflowExecutionRequestSearchAttributes(request, namespaceName) @@ -1952,12 +1955,16 @@ func (wh *WorkflowHandler) SignalWithStartWorkflowExecution(ctx context.Context, return nil, errWorkflowTypeTooLong } + if err := common.ValidateUTF8String("WorkflowType", request.WorkflowType.GetName()); err != nil { + return nil, err + } + if err := wh.validateTaskQueue(request.TaskQueue); err != nil { return nil, err } - if len(request.GetRequestId()) > wh.config.MaxIDLengthLimit() { - return nil, errRequestIDTooLong + if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil { + return nil, err } if err := wh.validateSignalWithStartWorkflowTimeouts(request); err != nil { @@ -4308,6 +4315,9 @@ func (wh *WorkflowHandler) validateTaskQueue(t *taskqueuepb.TaskQueue) error { if len(t.GetName()) > wh.config.MaxIDLengthLimit() { return errTaskQueueTooLong } + if err := common.ValidateUTF8String("TaskQueue", t.GetName()); err != nil { + return err + } enums.SetDefaultTaskQueueKind(&t.Kind) return nil @@ -4345,6 +4355,9 @@ func (wh *WorkflowHandler) validateBuildIdCompatibilityUpdate( errDeets = append(errDeets, fmt.Sprintf(" Worker build IDs to be no larger than %v characters", wh.config.WorkerBuildIdSizeLimit())) } + if err := common.ValidateUTF8String("BuildId", id); err != nil { + errDeets = append(errDeets, err.Error()) + } } if req.GetNamespace() == "" { @@ -4666,6 +4679,24 @@ func (wh *WorkflowHandler) validateRetryPolicy(namespaceName namespace.Name, ret return common.ValidateRetryPolicy(retryPolicy) } +func validateRequestId(requestID *string, lenLimit int) error { + if requestID == nil { + // should never happen, but just in case. + return serviceerror.NewInvalidArgument("RequestId is nil") + } + if *requestID == "" { + // For easy direct API use, we default the request ID here but expect all + // SDKs and other auto-retrying clients to set it + *requestID = uuid.New() + } + + if len(*requestID) > lenLimit { + return errRequestIDTooLong + } + + return common.ValidateUTF8String("RequestId", *requestID) +} + func (wh *WorkflowHandler) validateStartWorkflowTimeouts( request *workflowservice.StartWorkflowExecutionRequest, ) error { @@ -4777,7 +4808,7 @@ func (wh *WorkflowHandler) makeFakeContinuedAsNewEvent( func (wh *WorkflowHandler) validateNamespace( namespace string, ) error { - if err := wh.validateUTF8String(namespace); err != nil { + if err := common.ValidateUTF8String("Namespace", namespace); err != nil { return err } if len(namespace) > wh.config.MaxIDLengthLimit() { @@ -4792,7 +4823,7 @@ func (wh *WorkflowHandler) validateWorkflowID( if workflowID == "" { return errWorkflowIDNotSet } - if err := wh.validateUTF8String(workflowID); err != nil { + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { return err } if len(workflowID) > wh.config.MaxIDLengthLimit() {
service/frontend/workflow_handler_test.go+12 −0 modified@@ -2897,6 +2897,18 @@ func TestContextNearDeadline(t *testing.T) { assert.False(t, contextNearDeadline(ctx, time.Millisecond)) } +func TestValidateRequestId(t *testing.T) { + req := workflowservice.StartWorkflowExecutionRequest{RequestId: ""} + err := validateRequestId(&req.RequestId, 100) + assert.Nil(t, err) + assert.Len(t, req.RequestId, 36) // new UUID length + + req.RequestId = "\x87\x01" + err = validateRequestId(&req.RequestId, 100) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not a valid UTF-8 string") +} + func (s *workflowHandlerSuite) Test_DeleteWorkflowExecution() { config := s.newConfig() wh := s.getWorkflowHandler(config)
service/history/commandChecker.go+47 −1 modified@@ -391,6 +391,9 @@ func (v *commandAttrValidator) validateTimerScheduleAttributes( if len(attributes.GetTimerId()) > v.maxIDLengthLimit { return failedCause, serviceerror.NewInvalidArgument("TimerId exceeds length limit.") } + if err := common.ValidateUTF8String("TimerId", attributes.TimerId); err != nil { + return failedCause, err + } if timestamp.DurationValue(attributes.GetStartToFireTimeout()) <= 0 { return failedCause, serviceerror.NewInvalidArgument("A valid StartToFireTimeout is not set on command.") } @@ -509,7 +512,22 @@ func (v *commandAttrValidator) validateCancelExternalWorkflowExecutionAttributes if len(attributes.GetWorkflowId()) > v.maxIDLengthLimit { return failedCause, serviceerror.NewInvalidArgument("WorkflowId exceeds length limit.") } - runID := attributes.GetRunId() + runID := attributes.RunId + workflowID := attributes.WorkflowId + ns := attributes.Namespace + + if workflowID == "" { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId is not set on RequestCancelExternalWorkflowExecutionCommand. Namespace=%s RunId=%s", ns, runID)) + } + if len(ns) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("Namespace on RequestCancelExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s RunId=%s Namespace=%s Length=%d Limit=%d", workflowID, runID, ns, len(ns), v.maxIDLengthLimit)) + } + if len(workflowID) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId on RequestCancelExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Length=%d Limit=%d RunId=%s Namespace=%s", workflowID, len(workflowID), v.maxIDLengthLimit, runID, ns)) + } + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { + return failedCause, err + } if runID != "" && uuid.Parse(runID) == nil { return failedCause, serviceerror.NewInvalidArgument("Invalid RunId set on command.") } @@ -551,6 +569,22 @@ func (v *commandAttrValidator) validateSignalExternalWorkflowExecutionAttributes } targetRunID := attributes.Execution.GetRunId() + signalName := attributes.SignalName + workflowID := attributes.Execution.WorkflowId + ns := attributes.Namespace + + if workflowID == "" { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId is not set on SignalExternalWorkflowExecutionCommand. Namespace=%s RunId=%s SignalName=%s", ns, targetRunID, signalName)) + } + if len(ns) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("Namespace on SignalExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Namespace=%s Length=%d Limit=%d RunId=%s SignalName=%s", workflowID, ns, len(ns), v.maxIDLengthLimit, targetRunID, signalName)) + } + if len(workflowID) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId on SignalExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Length=%d Limit=%d Namespace=%s RunId=%s SignalName=%s", workflowID, len(workflowID), v.maxIDLengthLimit, ns, targetRunID, signalName)) + } + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { + return failedCause, err + } if targetRunID != "" && uuid.Parse(targetRunID) == nil { return failedCause, serviceerror.NewInvalidArgument("Invalid RunId set on command.") } @@ -724,6 +758,14 @@ func (v *commandAttrValidator) validateStartChildExecutionAttributes( return failedCause, serviceerror.NewInvalidArgument("WorkflowType exceeds length limit.") } + if err := common.ValidateUTF8String("WorkflowId", attributes.WorkflowId); err != nil { + return failedCause, err + } + + if err := common.ValidateUTF8String("WorkflowType", attributes.WorkflowType.Name); err != nil { + return failedCause, err + } + if timestamp.DurationValue(attributes.GetWorkflowExecutionTimeout()) < 0 { return failedCause, serviceerror.NewInvalidArgument("Invalid WorkflowExecutionTimeout.") } @@ -801,6 +843,10 @@ func (v *commandAttrValidator) validateTaskQueue( return taskQueue, serviceerror.NewInvalidArgument(fmt.Sprintf("task queue name exceeds length limit of %v", v.maxIDLengthLimit)) } + if err := common.ValidateUTF8String("TaskQueue", name); err != nil { + return taskQueue, err + } + if strings.HasPrefix(name, reservedTaskQueuePrefix) { return taskQueue, serviceerror.NewInvalidArgument(fmt.Sprintf("task queue name cannot start with reserved prefix %v", reservedTaskQueuePrefix)) }
2099dfd945acAdd validation for a few string fields (#5487)
6 files changed · +154 −17
common/searchattribute/encode_value.go+28 −3 modified@@ -25,15 +25,19 @@ package searchattribute import ( + "errors" "fmt" "time" + "unicode/utf8" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/server/common/payload" ) +var ErrInvalidString = errors.New("SearchAttribute value is not a valid UTF-8 string") + // EncodeValue encodes search attribute value and IndexedValueType to Payload. func EncodeValue(val interface{}, t enumspb.IndexedValueType) (*commonpb.Payload, error) { valPayload, err := payload.Encode(val) @@ -70,16 +74,37 @@ func DecodeValue( case enumspb.INDEXED_VALUE_TYPE_INT: return decodeValueTyped[int64](value, allowList) case enumspb.INDEXED_VALUE_TYPE_KEYWORD: - return decodeValueTyped[string](value, allowList) + return validateStrings(decodeValueTyped[string](value, allowList)) case enumspb.INDEXED_VALUE_TYPE_TEXT: - return decodeValueTyped[string](value, allowList) + return validateStrings(decodeValueTyped[string](value, allowList)) case enumspb.INDEXED_VALUE_TYPE_KEYWORD_LIST: - return decodeValueTyped[[]string](value, false) + return validateStrings(decodeValueTyped[[]string](value, false)) default: return nil, fmt.Errorf("%w: %v", ErrInvalidType, t) } } +func validateStrings(anyValue any, err error) (any, error) { + if err != nil { + return anyValue, err + } + + // validate strings + switch value := anyValue.(type) { + case string: + if !utf8.ValidString(value) { + return nil, fmt.Errorf("%w: %s", ErrInvalidString, value) + } + case []string: + for _, item := range value { + if !utf8.ValidString(item) { + return nil, fmt.Errorf("%w: %s", ErrInvalidString, item) + } + } + } + return anyValue, err +} + // decodeValueTyped tries to decode to the given type. // If the input is a list and allowList is false, then it will return only the first element. // If the input is a list and allowList is true, then it will return the decoded list.
common/searchattribute/encode_value_test.go+19 −0 modified@@ -25,6 +25,7 @@ package searchattribute import ( + "errors" "testing" "time" @@ -372,3 +373,21 @@ func Test_EncodeValue(t *testing.T) { "Datetime Search Attribute is expected to be encoded in RFC 3339 format") s.Equal("Datetime", string(encodedPayload.Metadata["type"])) } + +func Test_ValidateStrings(t *testing.T) { + _, err := validateStrings("anything here", errors.New("test error")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "test error") + + _, err = validateStrings("\x87\x01", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid UTF-8 string") + + value, err := validateStrings("anything here", nil) + assert.Nil(t, err) + assert.Equal(t, "anything here", value) + + _, err = validateStrings([]string{"abc", "\x87\x01"}, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is not a valid UTF-8 string") +}
common/util.go+8 −0 modified@@ -32,6 +32,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/dgryski/go-farm" "github.com/gogo/protobuf/proto" @@ -799,3 +800,10 @@ func OverrideWorkflowTaskTimeout( func CloneProto[T proto.Message](v T) T { return proto.Clone(v).(T) } + +func ValidateUTF8String(fieldName string, strValue string) error { + if !utf8.ValidString(strValue) { + return serviceerror.NewInvalidArgument(fmt.Sprintf("%s %v is not a valid UTF-8 string", fieldName, strValue)) + } + return nil +}
service/frontend/workflow_handler.go+38 −12 modified@@ -361,22 +361,20 @@ func (wh *WorkflowHandler) StartWorkflowExecution(ctx context.Context, request * return nil, errWorkflowTypeTooLong } - if err := wh.validateTaskQueue(request.TaskQueue); err != nil { + if err := common.ValidateUTF8String("WorkflowType", request.WorkflowType.GetName()); err != nil { return nil, err } - if err := wh.validateStartWorkflowTimeouts(request); err != nil { + if err := wh.validateTaskQueue(request.TaskQueue); err != nil { return nil, err } - if request.GetRequestId() == "" { - // For easy direct API use, we default the request ID here but expect all - // SDKs and other auto-retrying clients to set it - request.RequestId = uuid.New() + if err := wh.validateStartWorkflowTimeouts(request); err != nil { + return nil, err } - if len(request.GetRequestId()) > wh.config.MaxIDLengthLimit() { - return nil, errRequestIDTooLong + if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil { + return nil, err } request, err := wh.unaliasStartWorkflowExecutionRequestSearchAttributes(request, namespaceName) @@ -1992,12 +1990,16 @@ func (wh *WorkflowHandler) SignalWithStartWorkflowExecution(ctx context.Context, return nil, errWorkflowTypeTooLong } + if err := common.ValidateUTF8String("WorkflowType", request.WorkflowType.GetName()); err != nil { + return nil, err + } + if err := wh.validateTaskQueue(request.TaskQueue); err != nil { return nil, err } - if len(request.GetRequestId()) > wh.config.MaxIDLengthLimit() { - return nil, errRequestIDTooLong + if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil { + return nil, err } if err := wh.validateSignalWithStartWorkflowTimeouts(request); err != nil { @@ -4309,6 +4311,9 @@ func (wh *WorkflowHandler) validateTaskQueue(t *taskqueuepb.TaskQueue) error { if len(t.GetName()) > wh.config.MaxIDLengthLimit() { return errTaskQueueTooLong } + if err := common.ValidateUTF8String("TaskQueue", t.GetName()); err != nil { + return err + } enums.SetDefaultTaskQueueKind(&t.Kind) return nil @@ -4346,6 +4351,9 @@ func (wh *WorkflowHandler) validateBuildIdCompatibilityUpdate( errDeets = append(errDeets, fmt.Sprintf(" Worker build IDs to be no larger than %v characters", wh.config.WorkerBuildIdSizeLimit())) } + if err := common.ValidateUTF8String("BuildId", id); err != nil { + errDeets = append(errDeets, err.Error()) + } } if req.GetNamespace() == "" { @@ -4667,6 +4675,24 @@ func (wh *WorkflowHandler) validateRetryPolicy(namespaceName namespace.Name, ret return common.ValidateRetryPolicy(retryPolicy) } +func validateRequestId(requestID *string, lenLimit int) error { + if requestID == nil { + // should never happen, but just in case. + return serviceerror.NewInvalidArgument("RequestId is nil") + } + if *requestID == "" { + // For easy direct API use, we default the request ID here but expect all + // SDKs and other auto-retrying clients to set it + *requestID = uuid.New() + } + + if len(*requestID) > lenLimit { + return errRequestIDTooLong + } + + return common.ValidateUTF8String("RequestId", *requestID) +} + func (wh *WorkflowHandler) validateStartWorkflowTimeouts( request *workflowservice.StartWorkflowExecutionRequest, ) error { @@ -4778,7 +4804,7 @@ func (wh *WorkflowHandler) makeFakeContinuedAsNewEvent( func (wh *WorkflowHandler) validateNamespace( namespace string, ) error { - if err := wh.validateUTF8String(namespace); err != nil { + if err := common.ValidateUTF8String("Namespace", namespace); err != nil { return err } if len(namespace) > wh.config.MaxIDLengthLimit() { @@ -4793,7 +4819,7 @@ func (wh *WorkflowHandler) validateWorkflowID( if workflowID == "" { return errWorkflowIDNotSet } - if err := wh.validateUTF8String(workflowID); err != nil { + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { return err } if len(workflowID) > wh.config.MaxIDLengthLimit() {
service/frontend/workflow_handler_test.go+12 −0 modified@@ -2875,6 +2875,18 @@ func TestContextNearDeadline(t *testing.T) { assert.False(t, contextNearDeadline(ctx, time.Millisecond)) } +func TestValidateRequestId(t *testing.T) { + req := workflowservice.StartWorkflowExecutionRequest{RequestId: ""} + err := validateRequestId(&req.RequestId, 100) + assert.Nil(t, err) + assert.Len(t, req.RequestId, 36) // new UUID length + + req.RequestId = "\x87\x01" + err = validateRequestId(&req.RequestId, 100) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not a valid UTF-8 string") +} + func (s *workflowHandlerSuite) Test_DeleteWorkflowExecution() { config := s.newConfig() wh := s.getWorkflowHandler(config)
service/history/command_checker.go+49 −2 modified@@ -391,6 +391,9 @@ func (v *commandAttrValidator) validateTimerScheduleAttributes( if len(attributes.GetTimerId()) > v.maxIDLengthLimit { return failedCause, serviceerror.NewInvalidArgument("TimerId exceeds length limit.") } + if err := common.ValidateUTF8String("TimerId", attributes.GetTimerId()); err != nil { + return failedCause, err + } if timestamp.DurationValue(attributes.GetStartToFireTimeout()) <= 0 { return failedCause, serviceerror.NewInvalidArgument("A valid StartToFireTimeout is not set on command.") } @@ -509,7 +512,22 @@ func (v *commandAttrValidator) validateCancelExternalWorkflowExecutionAttributes if len(attributes.GetWorkflowId()) > v.maxIDLengthLimit { return failedCause, serviceerror.NewInvalidArgument("WorkflowId exceeds length limit.") } - runID := attributes.GetRunId() + runID := attributes.RunId + workflowID := attributes.WorkflowId + ns := attributes.Namespace + + if workflowID == "" { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId is not set on RequestCancelExternalWorkflowExecutionCommand. Namespace=%s RunId=%s", ns, runID)) + } + if len(ns) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("Namespace on RequestCancelExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s RunId=%s Namespace=%s Length=%d Limit=%d", workflowID, runID, ns, len(ns), v.maxIDLengthLimit)) + } + if len(workflowID) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId on RequestCancelExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Length=%d Limit=%d RunId=%s Namespace=%s", workflowID, len(workflowID), v.maxIDLengthLimit, runID, ns)) + } + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { + return failedCause, err + } if runID != "" && uuid.Parse(runID) == nil { return failedCause, serviceerror.NewInvalidArgument("Invalid RunId set on command.") } @@ -550,7 +568,24 @@ func (v *commandAttrValidator) validateSignalExternalWorkflowExecutionAttributes return failedCause, serviceerror.NewInvalidArgument("WorkflowId exceeds length limit.") } - targetRunID := attributes.Execution.GetRunId() + targetRunID := attributes.Execution.RunId + signalName := attributes.SignalName + workflowID := attributes.Execution.WorkflowId + ns := attributes.Namespace + + if workflowID == "" { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId is not set on SignalExternalWorkflowExecutionCommand. Namespace=%s RunId=%s SignalName=%s", ns, targetRunID, signalName)) + } + if len(ns) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("Namespace on SignalExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Namespace=%s Length=%d Limit=%d RunId=%s SignalName=%s", workflowID, ns, len(ns), v.maxIDLengthLimit, targetRunID, signalName)) + } + if len(workflowID) > v.maxIDLengthLimit { + return failedCause, serviceerror.NewInvalidArgument(fmt.Sprintf("WorkflowId on SignalExternalWorkflowExecutionCommand exceeds length limit. WorkflowId=%s Length=%d Limit=%d Namespace=%s RunId=%s SignalName=%s", workflowID, len(workflowID), v.maxIDLengthLimit, ns, targetRunID, signalName)) + } + if err := common.ValidateUTF8String("WorkflowId", workflowID); err != nil { + return failedCause, err + } + if targetRunID != "" && uuid.Parse(targetRunID) == nil { return failedCause, serviceerror.NewInvalidArgument("Invalid RunId set on command.") } @@ -724,6 +759,14 @@ func (v *commandAttrValidator) validateStartChildExecutionAttributes( return failedCause, serviceerror.NewInvalidArgument("WorkflowType exceeds length limit.") } + if err := common.ValidateUTF8String("WorkflowId", attributes.GetWorkflowId()); err != nil { + return failedCause, err + } + + if err := common.ValidateUTF8String("WorkflowType", attributes.WorkflowType.GetName()); err != nil { + return failedCause, err + } + if timestamp.DurationValue(attributes.GetWorkflowExecutionTimeout()) < 0 { return failedCause, serviceerror.NewInvalidArgument("Invalid WorkflowExecutionTimeout.") } @@ -801,6 +844,10 @@ func (v *commandAttrValidator) validateTaskQueue( return taskQueue, serviceerror.NewInvalidArgument(fmt.Sprintf("task queue name exceeds length limit of %v", v.maxIDLengthLimit)) } + if err := common.ValidateUTF8String("TaskQueue", name); err != nil { + return taskQueue, err + } + if strings.HasPrefix(name, reservedTaskQueuePrefix) { return taskQueue, serviceerror.NewInvalidArgument(fmt.Sprintf("task queue name cannot start with reserved prefix %v", reservedTaskQueuePrefix)) }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-wmxc-v39r-p9wfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-2689ghsaADVISORY
- github.com/temporalio/temporal/commit/2099dfd945accbf794404c3b8d990d109de19f06ghsaWEB
- github.com/temporalio/temporal/commit/679e3dc2ca8bd39e02c760f686cc8807f817bbfdghsaWEB
- github.com/temporalio/temporal/commit/f1fab97129f964dcca17d1f7c344f38666d1ee5fghsaWEB
News mentions
0No linked articles in our index yet.