Temporal write access can run shell on your Worker Service host

Temporal write access can run shell on your Worker Service host

How to fix CVE-2026-89139: upgrade Temporal Server to 1.31.3 or 1.30.7

4 min read964 words
Contents

Anyone with write access in a single Temporal namespace can make the Worker Service host run a shell command of their choosing. Temporal published CVE-2026-89139 today (CVSS 4.0 8.7 High). The Worker Controller's built-in subprocess compute provider takes the program and argv from the caller's worker-deployment request, not from operator config, and runs it immediately on the Worker Service machine. That process holds persistence credentials for every namespace and the cluster TLS material, so one namespace write reaches the whole cluster.

Ship the same upgrade for the sibling admin-callback bug CVE-2026-87858 (CVSS 4.0 7.2 High): a caller-controlled source header on a completion callback can retarget the History service at the internal frontend as system administrator. Do not open a second article for that train. This is the operator write-up; the HOL Guard evidence packs for CVE-2026-89139 and CVE-2026-87858 are the source records.

What breaks

On CVE-2026-89139, Temporal Server 1.31.0 through before 1.31.3 ships a Worker Controller Instance module inside the Worker Service. That module registers a compute provider named subprocess. An authenticated caller with only a write role in one namespace can set a worker deployment version whose scaling group uses that provider. The handler validates the spec, then invokes every provider with the invoke strategy right away. No scaling event, task arrival, or unusual request sequence is required. The official temporal-server binaries and container images include the provider.

The only gate that can keep subprocess unreachable is the per-namespace dynamic config workercontroller.compute_providers.enabled. Its default is an unset list, and the allowlist check is skipped when the value is unset, so every registered provider is permitted. The separate workercontroller.enabled flag defaults to false and does not gate this path. Temporal confirmed an affected release with that setting never present anywhere in dynamic config is still exposed.

On CVE-2026-87858, History decided whether a Workflow completion callback was "internal" by reading a caller-supplied HTTP header named source. A namespace writer who can attach a callback whose host matches component.callbacks.allowedAddresses, and who can put a non-empty source header on it (releases 1.30.0+), causes History to rewrite only scheme and host and send the caller's path, query, and body to the local frontend client. Where an internal frontend is deployed with its HTTP API enabled, that client authorizes every request as system administrator. Confirmed effects include terminating Workflows in other namespaces, registering namespaces, changing another namespace's config, and deleting another namespace and its Workflows. The same routing exists in both HSM and CHASM callback delivery. Releases 1.25.0 through 1.29.7 need the header to exactly match a configured cluster ID (harder). Upstream patches: PR 12021 (auto-scaled-workers bump for the subprocess fix), auto-scaled-workers PR 129, and PR 11965 (make Nexus callback source header inspection opt-in via callback.inspectSourceHeader, default false).

Who is not in scope

You are not in the CVE-2026-89139 blast radius if your Temporal Server is older than 1.31.0, or you are already on 1.31.3 / 1.32.0+, or the Worker Service is not running (non-default topologies only), or every namespace's workercontroller.compute_providers.enabled is set to an explicit list that omits subprocess. Deployments with no authorizer already grant every caller every namespace, so they have no namespace boundary for this bug to cross. For CVE-2026-87858, stock static topology without an internal frontend HTTP port, or an empty component.callbacks.allowedAddresses (the default denies all external callback URLs), keeps the admin retarget closed. Temporal Cloud is a separate product; this write-up is about self-hosted Temporal Server.

What this is not

Neither CVE is unauthenticated internet RCE. Both need an authenticated Temporal principal with namespace write. CVE-2026-89139 is host command execution on the Worker Service under the server process account. CVE-2026-87858 is admin-equivalent state changes through the internal frontend, not a free remote shell by itself. Neither is listed on CISA KEV as of this writing.

One operator check

On every self-hosted cluster, run the server binary or image tag check first:

temporal-server --version
# or
docker inspect --format '{{.Config.Image}}' temporal-frontend

Treat 1.31.0, 1.31.1, and 1.31.2 as CVE-2026-89139-exposed until you are on 1.31.3 or 1.32.0. For the callback sibling, treat 1.30.0 through before 1.30.7 and 1.31.0 through before 1.31.3 the same way; fixed trains are 1.30.7, 1.31.3, and 1.32.0.

Then confirm the dangerous defaults are not still live while you schedule the upgrade:

# dynamic config (per namespace). Unset = every compute provider allowed, including subprocess.
# Prefer an explicit allowlist that omits subprocess until you patch.
# workercontroller.compute_providers.enabled

# callback allowlist empty by default (deny). Non-empty + internal-frontend httpPort > 0 = 87858 exposure window.
# component.callbacks.allowedAddresses
# services.internal-frontend.rpc.httpPort

To look for a subprocess config already attached, call DescribeWorkerDeploymentVersion for each worker deployment version in each namespace and check whether any scaling group's compute provider type is subprocess. For callbacks already attached, DescribeWorkflowExecution returns registered completion callbacks. Note the frontend HTTP API logs method and URL at debug only, so missing log lines are not proof the admin path was unused.

How to fix

Upgrade Temporal Server to 1.31.3 (or 1.30.7 on the 1.30 train, or 1.32.0+):

# Helm example. Pin the chart/app version that ships temporal-server 1.31.3 or 1.32.0.
helm upgrade temporal temporalio/temporal --version <chart-with-1.31.3-or-1.32.0>

# Container
# Use the official image tags that embed temporal-server 1.31.3 / 1.30.7 / 1.32.0

Release notes: v1.30.7, v1.31.3, v1.32.0. After upgrade, re-check temporal-server --version, confirm no worker deployment version still advertises provider type subprocess unless you intentionally allow it post-patch, and leave callback.inspectSourceHeader at its patched default (false) unless you are in a documented mixed-version escape hatch that still needs legacy HTTP worker callbacks.

References

Continue reading

All posts