# RCA Report — CVE-2026-58138

## Summary

CVE-2026-58138 is an unauthenticated remote code execution vulnerability in
Orkes/Conductor-OSS (the community workflow orchestration engine) versions
**3.21.21 through before 3.30.2**. The `INLINE` system task (and the related
`LAMBDA`, `DO_WHILE`, and `SWITCH` task types) evaluates a user-supplied
JavaScript expression inside a GraalVM polyglot `Context` that is built with
**full host access** (`HostAccess.ALL`) and, for the Python evaluator,
`allowAllAccess(true)`. Because the community Conductor REST API performs **no
authentication by default**, any remote attacker who can reach the API can
register a workflow definition whose `INLINE` task carries a malicious
`expression`, start the workflow, and have the Conductor JVM execute arbitrary
operating-system commands. This was reproduced end-to-end against the real
`conductoross/conductor:3.22.3` Docker image: an unauthenticated
`POST /api/metadata/workflow` + `POST /api/workflow/{name}` caused
`Runtime.exec(["sh","-c",CMD])` to run **as root** inside the Conductor host,
and the command's stdout was returned verbatim in the task's `outputData.result`.

## Impact

- **Package/component affected:** `conductor-oss/conductor` —
  `core/src/main/java/com/netflix/conductor/core/events/ScriptEvaluator.java`
  (the GraalVM `js` evaluator context) and
  `core/src/main/java/com/netflix/conductor/core/execution/evaluators/PythonEvaluator.java`
  (the `python` evaluator context). Reachable through the `INLINE` workflow
  system task (`com.netflix.conductor.core.execution.tasks.Inline`) and
  `LAMBDA`/`DO_WHILE`/`SWITCH` which also evaluate script expressions.
- **Affected versions:** 3.21.21 … < 3.30.2 (confirmed on 3.22.3). The plain
  `allowHostAccess(HostAccess.ALL)` configuration is present through ~3.29.x;
  3.30.0/3.30.1 added a partial `denyAccess(...)` blocklist (reflection blocked
  but class loading still permitted); the complete fix lands in **3.30.2**.
- **Risk level and consequences:** **Critical** (CVSS 9.8,
  `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`). Full unauthenticated remote
  compromise of the orchestrator host: arbitrary command execution as the
  Conductor process user (root in the default Docker image), exposing the
  engine's persistence/queues, stored credentials, and every system its
  workflows touch.

## Impact Parity

- **Disclosed/claimed maximum impact:** Unauthenticated remote code execution
  (arbitrary OS command execution) — `code_execution`.
- **Reproduced impact from this run:** Unauthenticated remote code execution as
  **root** — the attacker-supplied shell command (`id; echo <marker>; hostname;
  cat /etc/os-release`) executed inside the Conductor host and its stdout
  (`uid=0(root)…`, the unique marker, the hostname, and the OS release line)
  was returned through the unauthenticated API. A second, independent proof
  confirmed a marker file was written to the container filesystem.
- **Parity:** `full`. The proof exercises the exact claimed surface
  (unauthenticated remote API → inline script → OS command execution) and
  demonstrates the claimed maximum impact (code execution as root).
- **Not demonstrated:** Nothing — the reproduction reached the full claimed
  impact, not merely a crash or memory-safety symptom.

## Root Cause

Conductor's `INLINE` task evaluates user-supplied script through
`ScriptEvaluator.eval()`, which builds a GraalVM polyglot `Context` for the
`js` language. In the vulnerable versions the context is created with **no
sandbox**:

```java
// core/.../events/ScriptEvaluator.java  (v3.22.3, createNewContext())
private static Context createNewContext() {
    return Context.newBuilder("js")
            .allowHostAccess(HostAccess.ALL)      // <-- every public Java method/field callable
            .option("engine.WarnInterpreterOnly", "false")
            .build();
}
```

`HostAccess.ALL` permits the script to call any public method or access any
public field on every Java host object it can reach. The `INLINE` task binds
its `inputParameters` map as the JavaScript global `$` — a **live Java object**
(`jsBindings.putMember("$", input)` in `ScriptEvaluator.eval()`). From `$` the
script bootstraps Java reflection without needing any `Java.type`/class-lookup
permission, because everything is reached by calling public methods on host
objects that `HostAccess.ALL` already exposes:

```js
var k  = $.getClass().getClass();                 // java.lang.Class
var S  = k.getMethod('getName').getReturnType();  // java.lang.String class
var fN = k.getMethod('forName', S);               // Class.forName(String)
var RT = fN.invoke(null, ['java.lang.Runtime']);  // java.lang.Runtime
var rt = RT.getMethod('getRuntime').invoke(null, []);
// build String[]{'sh','-c',CMD} reflectively, then:
var p  = RT.getMethod('exec', strArr.getClass()).invoke(rt, [strArr]);
// read p.getInputStream() -> return stdout as the task result
```

The Python evaluator is analogous:
`Context.newBuilder("python").allowAllAccess(true).build()`.

The Conductor **community REST API has no authentication by default**, so the
two calls that trigger evaluation — `POST /api/metadata/workflow` (register the
malicious workflow) and `POST /api/workflow/{name}` (start it, which
synchronously evaluates the `INLINE` task) — require no credentials. The task
result (`GET /api/workflow/{id}?includeTasks=true` →
`tasks[].outputData.result`) carries the executed command's stdout back to the
attacker.

**Fix commits (release 3.30.2):**
- `87a7d96aabbb706d6e84f812b93da5165028d18f` — replaces `HostAccess.ALL` with a
  builder that `denyAccess(...)` `Class`, `ClassLoader`, `java.lang.reflect.*`,
  `Runtime`, `ProcessBuilder`, `Process`, `System`, `Thread`, `ThreadGroup`;
  removes `allowAllAccess(true)` from `PythonEvaluator`.
- `c691e35e768caeb802c9f06ecdd9674c80081af1` — adds
  `allowHostClassLoading(false)`, `allowNativeAccess(false)`,
  `allowCreateThread(false)`, `allowCreateProcess(false)`,
  `allowIO(IOAccess.NONE)`, `allowEnvironmentAccess(EnvironmentAccess.NONE)`,
  and engine options `js.load=false` / `js.print=false` / `js.console=false`.
  Disabling host class loading is the binary discriminator that prevents the
  script from materializing the classes needed to reach `Runtime`.

## Reproduction Steps

1. **Reference script:** `bundle/repro/reproduction_steps.sh` (with helper
   `bundle/repro/exploit.py`).
2. **What the script does:**
   - Pulls the real Docker images `conductoross/conductor:3.22.3` (vulnerable)
     and `conductoross/conductor:3.30.2` (fixed negative control).
   - Starts the **vulnerable** Conductor with Docker `--network host`, waits for
     the `/health` endpoint to return 200, and confirms the unauthenticated
     metadata API (`GET /api/metadata/workflow` → HTTP 200, no auth).
   - Runs `exploit.py`, which (with no authentication) registers a workflow
     whose `INLINE` task carries the reflective `Runtime.exec` JavaScript
     payload, starts it, and polls the task result. The payload also writes a
     unique marker file inside the container.
   - Asserts the vulnerable task is `COMPLETED`, the result contains live
     command output, and the marker file exists inside the container.
   - Stops the vulnerable container, starts the **fixed** 3.30.2 image the same
     way, and runs the identical payload — asserting it is now **blocked**
     (`task_status=FAILED_WITH_TERMINAL_ERROR`, no command output, no marker
     file).
   - Writes `bundle/repro/runtime_manifest.json`.
3. **Expected evidence of reproduction:** `logs/vuln_exploit.log` /
   `artifacts/vuln_exploit.json` show `task_status=COMPLETED` and
   `rce_confirmed=true` with the command stdout (`uid=0(root)…`, the marker,
   hostname, OS release); `artifacts/vuln_marker_check.txt` shows the marker
   file present inside the vulnerable container. For the fixed build,
   `artifacts/fixed_exploit.json` shows `task_status=FAILED_WITH_TERMINAL_ERROR`,
   `rce_confirmed=false`, and `artifacts/fixed_marker_check.txt` shows the marker
   file absent. The script exits 0 only when both conditions hold.

## Evidence

- `bundle/logs/reproduction_steps.log` — full annotated run log.
- `bundle/logs/vuln_container.log` — vulnerable Conductor boot log.
- `bundle/artifacts/vuln_health.json` — `{"healthy":true}` from the vulnerable
  instance.
- `bundle/artifacts/vuln_metadata_http.txt` — `HTTP 200` (unauthenticated
  metadata API reachable).
- `bundle/artifacts/vuln_exploit.json` — exploit result:
  `register_status=200`, `start_status=200`, `workflow_status=COMPLETED`,
  `task_status=COMPLETED`, `rce_confirmed=true`, and
  `task_output_result` containing:
  ```
  uid=0(root) gid=0(root) groups=0(root)
  PRUVA_RCE_CONFIRMED_<ts>
  <hostname>
  PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
  ```
- `bundle/artifacts/vuln_marker_check.txt` — `docker exec` proof that the marker
  file was written inside the vulnerable container.
- `bundle/logs/fixed_container.log`, `bundle/artifacts/fixed_exploit.json` —
  fixed build: `task_status=FAILED_WITH_TERMINAL_ERROR`,
  `workflow_status=FAILED`, `rce_confirmed=false`, `task_output_result=null`.
- `bundle/artifacts/fixed_marker_check.txt` — marker file absent in the fixed
  container (`No such file or directory`).
- Environment: Docker `--network host` on host `muramasa` (Arch Linux kernel);
  sandbox reached the host-network conductor via the docker bridge gateway
  `172.20.0.1:8080`; vulnerable image `conductoross/conductor:3.22.3`,
  fixed image `conductoross/conductor:3.30.2`.

## Recommendations / Next Steps

- **Upgrade to Conductor ≥ 3.30.2** immediately. The `js`/`python` evaluators
  no longer run with host access / host class loading enabled.
- **Defense in depth:** put authentication/authorization in front of the
  Conductor API (the community build is unauthenticated by default); run the
  server as a non-root, least-privilege user; restrict which principals can
  register/run workflows.
- **Detection:** flag workflow definitions whose `INLINE`/`LAMBDA`/`DO_WHILE`/
  `SWITCH` tasks contain `expression` strings referencing `getClass`,
  `forName`, `Runtime`, `exec`, `ProcessBuilder`, or `java.` reflection; alert
  on Conductor processes spawning shells.
- **Testing:** add regression tests (as the fix did in `InlineTest.java`) that
  assert a reflection-based RCE expression yields `FAILED_WITH_TERMINAL_ERROR`
  for both `javascript` and `graaljs` evaluator types on every build.

## Additional Notes

- **Idempotency:** the script cleans up any leftover
  `conductor-vuln-repro`/`conductor-fixed-repro` containers at startup and
  stops/removes them after each phase, so it can be re-run cleanly. Each exploit
  uses a timestamped workflow name and marker, so repeated runs do not collide.
- **Networking:** in the DinD sandbox, Docker bridge port publishing (`-p`) does
  not expose a reachable port to the sandbox container; the script therefore
  uses `--network host` and reaches the conductor via the sandbox's default
  gateway (`detect_target_ip`), falling back to `127.0.0.1` when the sandbox is
  the docker host itself.
- **Scope of the proof:** the in-range version demonstrated (3.22.3) uses the
  plain `HostAccess.ALL` configuration; the 3.30.0/3.30.1 partial blocklist is
  not separately bypassed here — the negative control uses the complete fix
  (3.30.2) per the CVE's fixed version.
