# Variant RCA Report: CVE-2026-5199 — Temporal Server Batcher Worker Cross-Namespace Authorization Bypass

## Summary

No distinct bypass or alternate trigger was found on the fixed version (v1.29.5). Seven distinct variant hypotheses were systematically tested against both the vulnerable (v1.29.4) and fixed (v1.29.5) versions. All alternate triggers were blocked by the fix commit `90738c6200`. The fix comprehensively closes the cross-namespace authorization bypass by validating both `NamespaceId` and `Request.Namespace` against the worker-bound namespace before any downstream operation, and by using the worker-bound namespace for all internal frontend client calls.

## Fix Coverage / Assumptions

The original fix relies on the invariant that **a per-namespace batcher worker must only ever operate on its own bound namespace**. To enforce this, the fix makes three key assumptions:

1. **Both namespace identity forms must match**: `batchParams.NamespaceId` (UUID) and `batchParams.Request.Namespace` (name) must both equal the worker's bound namespace.
2. **The worker-bound namespace is authoritative**: After validation, `ns := a.namespace.String()` is used for all SDK client creation and `startTaskProcessorProtobuf` calls.
3. **String comparison is sufficient**: Exact Go string equality (`!=`) is used because Temporal namespace names and IDs are case-sensitive in the registry.

The fix explicitly covers:
- `BatchActivityWithProtobuf` activity entry point
- All batch operation types (SIGNAL, CANCEL, TERMINATE, RESET, DELETE, UPDATE_EXECUTION_OPTIONS, UNPAUSE_ACTIVITY, RESET_ACTIVITY, UPDATE_ACTIVITY_OPTIONS)
- The `BatchWorkflowProtobuf` workflow that invokes the activity

The fix does NOT cover:
- Direct invocation of `startTaskProcessorProtobuf` with a forged namespace parameter (but this function is unexported and unreachable without first bypassing `BatchActivityWithProtobuf`)
- Other per-namespace workers (scheduler, deployment, workerdeployment), which were reviewed and found not to have the same vulnerability pattern

## Variant / Alternate Trigger

Seven distinct variant hypotheses were tested:

1. **Nil Request bypass (Variant 1)**: Set `batchParams.Request = nil` to skip the `req != nil` check in `checkNamespaceProtobuf`. Result: **BLOCKED** on both versions — the code panics when accessing `Request` fields, and on v1.29.5 the workflow-level `ValidateBatchOperation` also blocks nil requests.

2. **Non-protobuf BatchActivity path (Variant 2)**: Exploit the legacy `BatchActivity` with `BatchParams.Namespace` set to a victim namespace. Result: **BLOCKED** on both versions — `BatchActivity` has always used `checkNamespace(batchParams.Namespace)` which validates the namespace name.

3. **Cancel operation type (Variant 3)**: Use `BATCH_OPERATION_TYPE_CANCEL` with mismatched namespace instead of SIGNAL. Result: **BYPASSED on v1.29.4, BLOCKED on v1.29.5** — the same root cause affects all operation types, and the fix covers all of them via `checkNamespaceProtobuf`.

4. **Case-insensitive namespace (Variant 4)**: Use `"BOUND-NS"` (different case) to try to bypass string comparison. Result: **BYPASSED on v1.29.4, BLOCKED on v1.29.5** — `checkNamespaceProtobuf` uses exact string comparison, and Temporal namespace registry lookups are also case-sensitive.

5. **Reset operation type (Variant 5)**: Use `BATCH_OPERATION_TYPE_RESET` with mismatched namespace, targeting the `getResetEventIDByType` path. Result: **BYPASSED on v1.29.4, BLOCKED on v1.29.5** — the fix's belt-and-suspenders change to use the `namespace` parameter in the reset path is effective.

6. **Direct `startTaskProcessorProtobuf` namespace injection (Variant 6)**: Call `startTaskProcessorProtobuf` directly with a forged `namespace` parameter. Result: **Would use forged namespace** — but this is not a real bypass because the function is unexported and only reachable through `BatchActivityWithProtobuf`, which validates first.

7. **Empty namespace string (Variant 7)**: Set `Request.Namespace = ""` with a valid `NamespaceId`. Result: **BYPASSED on v1.29.4, BLOCKED on v1.29.5** — `checkNamespaceProtobuf` catches the empty string mismatch.

8. **Original CVE path (reference)**: SIGNAL with `NamespaceId=boundNSID` and `Request.Namespace=otherNSName`. Result: **BYPASSED on v1.29.4, BLOCKED on v1.29.5**.

**Entry points tested**: All variants go through `BatchActivityWithProtobuf` in `service/worker/batcher/activities.go`, which is the same entry point as the original CVE.

## Impact

- **Package/Component**: `go.temporal.io/server/service/worker/batcher`
- **Affected versions (original bug)**: `1.29.0` – `1.29.4`, `1.30.0` – `1.30.2`
- **Fixed versions**: `v1.29.5`, `v1.30.3`
- **Risk level**: No new risk identified on fixed versions. The fix is comprehensive.

## Root Cause

The original root cause was that `BatchActivityWithProtobuf` validated only `batchParams.NamespaceId` (a UUID) via `checkNamespaceID`, but then forwarded `batchParams.Request.Namespace` (a name) to the internal frontend client. Because the internal frontend runs with `NoopClaimClaimMapper → RoleAdmin`, any namespace name supplied by an attacker was executed unconditionally.

The fix closes this by validating **both** identity forms and using the **worker-bound namespace** for all downstream operations. No alternate path was found that could bypass these checks.

## Reproduction Steps

The variant reproduction script is `vuln_variant/reproduction_steps.sh`. It performs the following:

1. Copies `vuln_variant/variant_test.go` into `service/worker/batcher/variant_test.go`.
2. Runs `go test -v -run '^TestVariant_' ./...` against `v1.29.4` (vulnerable) and `v1.29.5` (fixed).
3. Captures output to `logs/variant_v1.29.4.log` and `logs/variant_v1.29.5.log`.
4. Greps for `BYPASSED` vs `BLOCKED` to determine if any variant bypassed the fixed version.

Run the script:
```bash
./vuln_variant/reproduction_steps.sh
```

Expected behavior:
- On v1.29.4: several variants show `BYPASSED` (demonstrating the original vulnerability affects multiple operation types and edge cases)
- On v1.29.5: all variants show `BLOCKED` (the fix is comprehensive)

## Evidence

### v1.29.4 (vulnerable) test results
Log location: `logs/variant_v1.29.4.log`

Key excerpts:
```
VARIANT3_CANCEL_BYPASSED
VARIANT4_CASE_BYPASSED
VARIANT5_RESET_BYPASSED with capturedNs=other-ns
VARIANT7_EMPTY_NS_BYPASSED
ORIGINAL_CVE_BYPASSED capturedNs=other-ns
```

### v1.29.5 (fixed) test results
Log location: `logs/variant_v1.29.5.log`

Key excerpts:
```
VARIANT3_CANCEL_BLOCKED: activity error ... namespace mismatch
VARIANT4_CASE_BLOCKED: activity error ... namespace mismatch
VARIANT5_RESET_BLOCKED: activity error ... namespace mismatch
VARIANT7_EMPTY_NS_BLOCKED: activity error ... namespace mismatch
ORIGINAL_CVE_BLOCKED: activity error ... namespace mismatch
```

## Recommendations / Next Steps

1. **The fix is complete**: No additional code changes are required to address the reported CVE.

2. **Defense in depth**: Consider adding namespace validation at the internal frontend client boundary so that even if a worker misbehaves, the internal frontend rejects cross-namespace requests.

3. **Regression testing**: The regression tests added in commit `90738c6200` should be preserved in all release branches. Do not relax `checkNamespaceProtobuf` or remove the worker-bound namespace derivation (`ns := a.namespace.String()`).

4. **Periodic audit**: Audit other per-namespace workers annually for similar ID-vs-name validation gaps, especially when new protobuf-based activities are added.

## Additional Notes

- **Idempotency**: The reproduction script is idempotent. It restores the original git ref and cleans up the copied test file on exit via a `trap`.
- **Test methodology**: Tests are unit tests in the `batcher` package using the Temporal SDK's `TestActivityEnvironment` and gomock for the frontend client and SDK client factory.
- **No real bypass found**: All 7 variant hypotheses were blocked on v1.29.5. The exit code of `vuln_variant/reproduction_steps.sh` is 1, indicating no bypass.
