## Ticket: CVE-2026-5199 — Temporal Server batcher worker cross-namespace authorization bypass (BatchActivityWithProtobuf)

**CVE**: CVE-2026-5199 | **CWE-285** (Improper Authorization) | **CVSS 3.1**: ~4.2 (Authenticated, Medium)
**Vendor / Product**: Temporal Technologies — Temporal Server (Go workflow engine)
**Repository**: https://github.com/temporalio/temporal
**Affected**: `1.29.0` – `1.29.4` (and `1.30.0` – `1.30.2`) | **Fixed**: `1.29.5`
**NVD**: https://nvd.nist.gov/vuln/detail/CVE-2026-5199
**Fix commit**: https://github.com/temporalio/temporal/commit/90738c6200 ("Check namespaces in batch workflow")
**Release**: https://github.com/temporalio/temporal/releases/tag/v1.29.5
**Cited on**: https://red.anthropic.com/2026/cvd/

### Impact

The original NVD blurb and several early advisories claimed a **frontend gRPC cross-namespace signal bypass**. That vector is **incorrect** — the public frontend authorization interceptor correctly validates `Request.Namespace` against the caller's claims on both v1.29.4 and v1.29.5.

The *actual* vulnerability lives in the **per-namespace batcher worker** (`service/worker/batcher/activities.go`). `BatchActivityWithProtobuf` receives a `BatchOperationInput` protobuf and, on v1.29.4, calls `checkNamespaceID(batchParams.NamespaceId)`. This validates only the namespace ID field, but the activity then forwards `batchParams.Request.Namespace` (a namespace **name**) to the internal frontend client. The internal frontend runs with `NoopClaimMapper → RoleAdmin`, so any namespace string supplied by the attacker is executed unconditionally.

An attacker who holds a valid auth token (writer role) for **namespace A** can submit a batch operation whose protobuf carries:
- `NamespaceId` = the worker-bound ID for namespace A (passes `checkNamespaceID`)
- `Request.Namespace` = the **name** of victim namespace B

The batcher worker then signals, cancels, terminates, or resets workflows in namespace B without re-validating the caller's authorization for B.

The bug is **authenticated** (requires valid credentials for at least one namespace on the cluster). It affects multi-tenant clusters that expose batch operations. The impact is an integrity violation against victim workflows (signal/reset/terminate), not data exfiltration.

### Affected / Fixed

| | Version |
|--|--|
| **Vulnerable** | `1.29.0` – `1.29.4` (last vulnerable tag: `v1.29.4`) |
| **Fixed** | `1.29.5` (tag `v1.29.5`) |

### Where the patch lives

```
git clone https://github.com/temporalio/temporal external/temporal
cd external/temporal
git log --oneline v1.29.4..v1.29.5 -- service/worker/batcher/activities.go
```

Commit `90738c6200` ("Check namespaces in batch workflow") makes three changes:

1. **Replaces `checkNamespaceID` with `checkNamespaceProtobuf`** — the new helper validates **both**:
   - `batchParams.NamespaceId == a.namespaceID.String()`
   - `batchParams.Request.GetNamespace() == a.namespace.String()`
2. **Changes `BatchActivityWithProtobuf`** to call `checkNamespaceProtobuf(batchParams)` first, then derive `ns := a.namespace.String()` and use that trusted worker-bound name for SDK client creation and all downstream frontend client calls.
3. **Belt-and-suspenders in `startTaskProcessorProtobuf`** — the `ResetOperation` path now uses the already-validated `namespace` parameter instead of `batchOperation.Request.Namespace`.

### Reproduction plan

The reproduction must hit the **batcher worker** code path (`BatchActivityWithProtobuf`), not the public frontend gRPC endpoint. A Go integration test in `service/worker/batcher/` against the affected commit is sufficient.

1. Clone and check out the vulnerable ref:
   ```
   git clone https://github.com/temporalio/temporal external/temporal
   cd external/temporal
   git checkout v1.29.4
   ```

2. The test harness should instantiate `batchActivities` with:
   - a mock `frontendClient` (implements `workflowservice.WorkflowServiceClient`) that records which `Namespace` it receives
   - a bound `namespace` and `namespaceID` representing the attacker's namespace (e.g., `"attacker-ns"` / `"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"`)

3. Construct a `BatchOperationInput` protobuf where:
   - `NamespaceId` matches the bound `namespaceID`
   - `Request.Namespace` is set to a **different** victim namespace name (e.g., `"victim-ns"`)
   - The operation type is `SIGNAL`, `CANCEL`, `TERMINATE`, or `RESET`

4. Invoke `BatchActivityWithProtobuf` with this payload.

5. **Vulnerable behavior (`v1.29.4`)**: `checkNamespaceID` returns `nil` because `NamespaceId` matches. The activity proceeds to `startTaskProcessorProtobuf(..., batchParams.Request.Namespace, ...)`, and the mock `frontendClient` receives `Namespace: "victim-ns"`. The operation would execute against the victim namespace.

6. **Fixed behavior (`v1.29.5`)**: `checkNamespaceProtobuf` returns an error because `Request.Namespace` does not match the worker-bound namespace. The call is blocked before any frontend client interaction.

7. Repeat the same test at `v1.29.5` to confirm the mismatch is rejected.

### Expected reproduction artifacts

- `repro/reproduction_steps.sh` — clones temporal at `v1.29.4` and `v1.29.5`, copies a Go test into `service/worker/batcher/`, runs `go test -v -run 'TestVariant_' ./service/worker/batcher/`, and compares the verdicts.
- `repro/validation_verdict.json` — `confirmed` only if (a) the v1.29.4 test shows `checkNamespaceID` allowing a mismatched `Request.Namespace` and the mock frontend client receives the victim namespace, AND (b) the v1.29.5 test shows `checkNamespaceProtobuf` rejecting the same mismatch.
- `logs/vulnerable_variant.txt` — `go test` output from the v1.29.4 run showing `CHECK_NAMESPACE_ID_ALLOWED_MISMATCH`.
- `logs/fixed_variant.txt` — `go test` output from the v1.29.5 run showing `CHECK_NAMESPACE_PROTOBUF_DENIED` and `CHECK_NAMESPACE_PROTOBUF_MATCH_ALLOWED`.
- `repro/rca_report.md` — RCA explaining the difference between `checkNamespaceID` (validates only ID) and `checkNamespaceProtobuf` (validates both ID and name), and how the internal frontend's `NoopClaimMapper → RoleAdmin` credential allows the forged namespace name to execute unconditionally.
