Temporal 的掩码命名空间漏洞
Masked namespace vulnerability in Temporal

原始链接: https://depthfirst.com/post/the-masked-namespace-vulnerability-in-temporal-cve-2025-14986

## Temporal 漏洞 (CVE-2025-14986) 概要 depthfirst 的安全研究人员发现 Temporal 中存在一个关键的身份绑定漏洞,Temporal 是一种耐用执行系统,被 Netflix 和 Stripe 等公司使用。该漏洞存在于 `ExecuteMultiOperation` 端点,该端点为了提高效率,将多个操作(如启动和更新工作流)捆绑到一个请求中。 该漏洞源于不一致的命名空间处理。虽然外部请求被正确授权,但捆绑内的内部操作包含自己的命名空间信息。Temporal 错误地将此内部命名空间用于策略和模式评估,从而创建了“混淆代理”场景。 这使得攻击者能够在多租户设置中绕过租户隔离——访问和操纵其他租户的配置——并启动“自带策略”攻击,覆盖组织治理控制。 Temporal 通过强制执行一个规则来解决此问题,即内部操作的命名空间*必须*与授权的外部命名空间匹配。该修复已在 1.27 版本中发布,强调了捆绑 API 的复杂性风险以及一致的身份管理的重要性。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Temporal 中掩码命名空间漏洞 (depthfirst.com) 6 点赞 来自 bmit 1 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Developers love "bundled" APIs. They offer atomicity and efficiency, allowing you to chain complex state changes into a single network request. Security engineers, however, should fear them. Bundling introduces complexity, and complexity is where the bugs hide.

As part of my research at depthfirst, I recently discovered a vulnerability in Temporal’s ExecuteMultiOperation endpoint (CVE-2025-14986). It was an identity-binding bug: the outer request passed authorization for one namespace, but an inner operation carried a different namespace that the server used during request preparation.

Why This Matters (What is Temporal?)

For those unfamiliar, Temporal is the backbone of durable execution for companies like Netflix, Stripe, and Datadog. It ensures code runs reliably even if servers fail. When you find a bug in Temporal, it affects the reliability layer that major companies depend on.

City Gate

The vulnerability lived in ExecuteMultiOperation, a handler designed to execute a StartWorkflow and UpdateWorkflow command in a single transaction.

When a request hits this endpoint, Temporal correctly performs an authorization check on the outer namespace. If I am authenticated as AttackerNS, the system checks my permissions, resolves my namespaceID, and opens the gate.

// service/frontend/workflow_handler.go

func (wh *WorkflowHandler) ExecuteMultiOperation(
    ctx context.Context,
    request *workflowservice.ExecuteMultiOperationRequest,
) (_ *workflowservice.ExecuteMultiOperationResponse, retError error) {
    // ...
    // 1. AUTHORIZATION: The system validates the top-level namespace
    namespaceName := namespace.Name(request.Namespace)
    namespaceID, err := wh.namespaceRegistry.GetNamespaceID(namespaceName)
    // ...
    // 2. HANDOFF: The derived namespaceID is passed downstream
    historyReq, err := wh.convertToHistoryMultiOperationRequest(namespaceID, request)

So far, so good. The guard checked my ID, and I was allowed in.

Two Faces

The problem arises when the system unpacks the bundle. The ExecuteMultiOperation request contains a list of operations. These inner operations carry their own metadata, including a Namespace field.

In the helper function convertToHistoryMultiOperationItem, the logic splits. The code had two different sources of truth for "Who is this user?":

  1. The Verified Identity: The namespaceID passed down from the authorization check.
  2. The Untrusted Identity: The namespace in the startReq JSON payload inside the bundle.

The bug was a discrepancy between which identity was used for request preparation (policies/aliases/schema) versus which identity was used for routing and persistence:

// service/frontend/workflow_handler.go

func (wh *WorkflowHandler) convertToHistoryMultiOperationItem(
    namespaceID namespace.ID, // <--- The Verified Source (Attacker)
    op *workflowservice.ExecuteMultiOperationRequest_Operation,
) (*historyservice.ExecuteMultiOperationRequest_Operation, string, error) {
    // ...
    if startReq := op.GetStartWorkflow(); startReq != nil {
        var err error
        
        // VULNERABILITY PART 1: The Logic Check
        // The system uses the UNTRUSTED payload to calculate policies and aliases.
        // It asks: "Does this payload conform to the rules of startReq.Namespace (Victim)?"
        if startReq, err = wh.prepareStartWorkflowRequest(startReq); err != nil {
            return nil, "", err
        }

        // ...

        opReq = &historyservice.ExecuteMultiOperationRequest_Operation{
            Operation: &historyservice.ExecuteMultiOperationRequest_Operation_StartWorkflow{
                // VULNERABILITY PART 2: The Routing
                // The system uses the VERIFIED ID to decide where to save the data.
                StartWorkflow: common.CreateHistoryStartWorkflowRequest(
                    namespaceID.String(), 
                    startReq, // ...but passes the payload configured for the Victim.
                    nil,
                    nil,
                    time.Now().UTC(),
                ),
            },
        }
    // ...
    }
}

Exploit

This vulnerability created a "Confused Deputy" scenario. We could pass authorization under one namespace, while influencing policy/schema evaluation using another.

Here's the structure of the exploit JSON request:

{
  // The Envelope (Authorized & Verified)
  "namespace": "AttackerNS", 
  "operations": [
    {
      "startWorkflow": {
        // The Payload (Untrusted)
        // The system uses THIS field to look up policies and schema
        "namespace": "VictimNS", 
        // ...
      }
    }
    // ...
  ]
}

I found two ways to exploit this mismatch:

1. Cross-Tenant Isoloation Breach

In a multi-tenant SaaS, Tenant A should never be able to interact with the configuration of Tenant B.

  • Setup: I created a VictimNS with a unique, private database schema (custom Search Attributes).
  • Breach: I sent a request as AttackerNS but referenced the VictimNS in the payload. The system validated our data against the Victim's private schema but saved the result in our database.
  • Impact: I forced the system to cross the tenant boundary to resolve our data. Temporal "touched" the Victim's configuration to process our request, breaking tenant isolation.

2. "Bring Your Own Policy" Attack 

In many organizations, Production and Dev environments are locked down with strict policies—execution timeouts, retry limits, and archival settings. A rogue developer cannot override these Temporal policies set by the organization admin.

But with this exploit, they can. A developer can authenticate validly against the Corporate Org, but point to the policy configuration of their Personal Account.

  • Setup: CorporateNS enforces strict governance (e.g., max execution time, rate limits). The developer's PersonalNS is fully permissive.
  • Breach: The developer sends a request authorized for CorporateNS, but the inner payload specifies PersonalNS. The system validates the request against the permissive PersonalNS rules.
  • Impact: The workflow executes within the corporate environment but ignores its rules. The developer has effectively injected a shadow policy to override the organization admin's controls.

Patch & Remediation

The masked namespace worked because the server verified the mask on the outside, then trusted the face inside the bundle.

This vulnerability existed because the system accepted two different namespace identities inside a single request. Authorization was performed once on the outer namespace, but request preparation later trusted the inner namespace when deriving policies and schema. With the release of v1.27, Temporal enforces a simple invariant: the namespace referenced by inner operations must match the outer, authorized namespace.

The fix introduces a check to ensure Outer.ID == Inner.ID before any processing occurs:

// service/frontend/workflow_handler.go (Patched)

if startReq := op.GetStartWorkflow(); startReq != nil {
    // [FIX] Validate that inner namespace matches outer authorized namespace
    if startReq.Namespace != "" && startReq.Namespace != namespaceName.String() {
        return nil, "", errMultiOpNamespaceMismatch
    }
    // ...
}

Timeline

  • Dec 12, 2025: Vulnerability reported to Temporal Security.
  • Dec 16, 2025: Patch committed internally (Commit cd79be6).
  • Dec 18, 2025: Validation of fix.
  • Dec 30, 2025: Public release of Temporal Server v1.27.x/1.28.x/1.29.x and CVE-2025-14986.
  • Jan 05, 2026: security.txt updated with public credit.
联系我们 contact @ memedata.com