{"repro_id":"REPRO-2026-00236","version":6,"title":"Orkes Conductor unauthenticated RCE via inline script evaluators","repro_type":"security","status":"published","severity":"critical","description":"Orkes Conductor OSS before v3.30.2 contains an unauthenticated RCE vulnerability (CVE-2026-58138, CVSS 9.8) where remote attackers submit inline workflow definitions with malicious JavaScript or Python expressions to the workflow API endpoint without authentication. Unsandboxed GraalVM script evaluators configured with HostAccess.ALL or allowAllAccess(true) allow arbitrary OS command execution via Java reflection or direct subprocess calls. Affected task types: INLINE, LAMBDA, DO_WHILE, SWITCH.","root_cause":"# RCA Report — CVE-2026-58138\n\n## Summary\n\nCVE-2026-58138 is an unauthenticated remote code execution vulnerability in\nOrkes/Conductor-OSS (the community workflow orchestration engine) versions\n**3.21.21 through before 3.30.2**. The `INLINE` system task (and the related\n`LAMBDA`, `DO_WHILE`, and `SWITCH` task types) evaluates a user-supplied\nJavaScript expression inside a GraalVM polyglot `Context` that is built with\n**full host access** (`HostAccess.ALL`) and, for the Python evaluator,\n`allowAllAccess(true)`. Because the community Conductor REST API performs **no\nauthentication by default**, any remote attacker who can reach the API can\nregister a workflow definition whose `INLINE` task carries a malicious\n`expression`, start the workflow, and have the Conductor JVM execute arbitrary\noperating-system commands. This was reproduced end-to-end against the real\n`conductoross/conductor:3.22.3` Docker image: an unauthenticated\n`POST /api/metadata/workflow` + `POST /api/workflow/{name}` caused\n`Runtime.exec([\"sh\",\"-c\",CMD])` to run **as root** inside the Conductor host,\nand the command's stdout was returned verbatim in the task's `outputData.result`.\n\n## Impact\n\n- **Package/component affected:** `conductor-oss/conductor` —\n  `core/src/main/java/com/netflix/conductor/core/events/ScriptEvaluator.java`\n  (the GraalVM `js` evaluator context) and\n  `core/src/main/java/com/netflix/conductor/core/execution/evaluators/PythonEvaluator.java`\n  (the `python` evaluator context). Reachable through the `INLINE` workflow\n  system task (`com.netflix.conductor.core.execution.tasks.Inline`) and\n  `LAMBDA`/`DO_WHILE`/`SWITCH` which also evaluate script expressions.\n- **Affected versions:** 3.21.21 … < 3.30.2 (confirmed on 3.22.3). The plain\n  `allowHostAccess(HostAccess.ALL)` configuration is present through ~3.29.x;\n  3.30.0/3.30.1 added a partial `denyAccess(...)` blocklist (reflection blocked\n  but class loading still permitted); the complete fix lands in **3.30.2**.\n- **Risk level and consequences:** **Critical** (CVSS 9.8,\n  `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`). Full unauthenticated remote\n  compromise of the orchestrator host: arbitrary command execution as the\n  Conductor process user (root in the default Docker image), exposing the\n  engine's persistence/queues, stored credentials, and every system its\n  workflows touch.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Unauthenticated remote code execution\n  (arbitrary OS command execution) — `code_execution`.\n- **Reproduced impact from this run:** Unauthenticated remote code execution as\n  **root** — the attacker-supplied shell command (`id; echo <marker>; hostname;\n  cat /etc/os-release`) executed inside the Conductor host and its stdout\n  (`uid=0(root)…`, the unique marker, the hostname, and the OS release line)\n  was returned through the unauthenticated API. A second, independent proof\n  confirmed a marker file was written to the container filesystem.\n- **Parity:** `full`. The proof exercises the exact claimed surface\n  (unauthenticated remote API → inline script → OS command execution) and\n  demonstrates the claimed maximum impact (code execution as root).\n- **Not demonstrated:** Nothing — the reproduction reached the full claimed\n  impact, not merely a crash or memory-safety symptom.\n\n## Root Cause\n\nConductor's `INLINE` task evaluates user-supplied script through\n`ScriptEvaluator.eval()`, which builds a GraalVM polyglot `Context` for the\n`js` language. In the vulnerable versions the context is created with **no\nsandbox**:\n\n```java\n// core/.../events/ScriptEvaluator.java  (v3.22.3, createNewContext())\nprivate static Context createNewContext() {\n    return Context.newBuilder(\"js\")\n            .allowHostAccess(HostAccess.ALL)      // <-- every public Java method/field callable\n            .option(\"engine.WarnInterpreterOnly\", \"false\")\n            .build();\n}\n```\n\n`HostAccess.ALL` permits the script to call any public method or access any\npublic field on every Java host object it can reach. The `INLINE` task binds\nits `inputParameters` map as the JavaScript global `$` — a **live Java object**\n(`jsBindings.putMember(\"$\", input)` in `ScriptEvaluator.eval()`). From `$` the\nscript bootstraps Java reflection without needing any `Java.type`/class-lookup\npermission, because everything is reached by calling public methods on host\nobjects that `HostAccess.ALL` already exposes:\n\n```js\nvar k  = $.getClass().getClass();                 // java.lang.Class\nvar S  = k.getMethod('getName').getReturnType();  // java.lang.String class\nvar fN = k.getMethod('forName', S);               // Class.forName(String)\nvar RT = fN.invoke(null, ['java.lang.Runtime']);  // java.lang.Runtime\nvar rt = RT.getMethod('getRuntime').invoke(null, []);\n// build String[]{'sh','-c',CMD} reflectively, then:\nvar p  = RT.getMethod('exec', strArr.getClass()).invoke(rt, [strArr]);\n// read p.getInputStream() -> return stdout as the task result\n```\n\nThe Python evaluator is analogous:\n`Context.newBuilder(\"python\").allowAllAccess(true).build()`.\n\nThe Conductor **community REST API has no authentication by default**, so the\ntwo calls that trigger evaluation — `POST /api/metadata/workflow` (register the\nmalicious workflow) and `POST /api/workflow/{name}` (start it, which\nsynchronously evaluates the `INLINE` task) — require no credentials. The task\nresult (`GET /api/workflow/{id}?includeTasks=true` →\n`tasks[].outputData.result`) carries the executed command's stdout back to the\nattacker.\n\n**Fix commits (release 3.30.2):**\n- `87a7d96aabbb706d6e84f812b93da5165028d18f` — replaces `HostAccess.ALL` with a\n  builder that `denyAccess(...)` `Class`, `ClassLoader`, `java.lang.reflect.*`,\n  `Runtime`, `ProcessBuilder`, `Process`, `System`, `Thread`, `ThreadGroup`;\n  removes `allowAllAccess(true)` from `PythonEvaluator`.\n- `c691e35e768caeb802c9f06ecdd9674c80081af1` — adds\n  `allowHostClassLoading(false)`, `allowNativeAccess(false)`,\n  `allowCreateThread(false)`, `allowCreateProcess(false)`,\n  `allowIO(IOAccess.NONE)`, `allowEnvironmentAccess(EnvironmentAccess.NONE)`,\n  and engine options `js.load=false` / `js.print=false` / `js.console=false`.\n  Disabling host class loading is the binary discriminator that prevents the\n  script from materializing the classes needed to reach `Runtime`.\n\n## Reproduction Steps\n\n1. **Reference script:** `bundle/repro/reproduction_steps.sh` (with helper\n   `bundle/repro/exploit.py`).\n2. **What the script does:**\n   - Pulls the real Docker images `conductoross/conductor:3.22.3` (vulnerable)\n     and `conductoross/conductor:3.30.2` (fixed negative control).\n   - Starts the **vulnerable** Conductor with Docker `--network host`, waits for\n     the `/health` endpoint to return 200, and confirms the unauthenticated\n     metadata API (`GET /api/metadata/workflow` → HTTP 200, no auth).\n   - Runs `exploit.py`, which (with no authentication) registers a workflow\n     whose `INLINE` task carries the reflective `Runtime.exec` JavaScript\n     payload, starts it, and polls the task result. The payload also writes a\n     unique marker file inside the container.\n   - Asserts the vulnerable task is `COMPLETED`, the result contains live\n     command output, and the marker file exists inside the container.\n   - Stops the vulnerable container, starts the **fixed** 3.30.2 image the same\n     way, and runs the identical payload — asserting it is now **blocked**\n     (`task_status=FAILED_WITH_TERMINAL_ERROR`, no command output, no marker\n     file).\n   - Writes `bundle/repro/runtime_manifest.json`.\n3. **Expected evidence of reproduction:** `logs/vuln_exploit.log` /\n   `artifacts/vuln_exploit.json` show `task_status=COMPLETED` and\n   `rce_confirmed=true` with the command stdout (`uid=0(root)…`, the marker,\n   hostname, OS release); `artifacts/vuln_marker_check.txt` shows the marker\n   file present inside the vulnerable container. For the fixed build,\n   `artifacts/fixed_exploit.json` shows `task_status=FAILED_WITH_TERMINAL_ERROR`,\n   `rce_confirmed=false`, and `artifacts/fixed_marker_check.txt` shows the marker\n   file absent. The script exits 0 only when both conditions hold.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full annotated run log.\n- `bundle/logs/vuln_container.log` — vulnerable Conductor boot log.\n- `bundle/artifacts/vuln_health.json` — `{\"healthy\":true}` from the vulnerable\n  instance.\n- `bundle/artifacts/vuln_metadata_http.txt` — `HTTP 200` (unauthenticated\n  metadata API reachable).\n- `bundle/artifacts/vuln_exploit.json` — exploit result:\n  `register_status=200`, `start_status=200`, `workflow_status=COMPLETED`,\n  `task_status=COMPLETED`, `rce_confirmed=true`, and\n  `task_output_result` containing:\n  ```\n  uid=0(root) gid=0(root) groups=0(root)\n  PRUVA_RCE_CONFIRMED_<ts>\n  <hostname>\n  PRETTY_NAME=\"Debian GNU/Linux 13 (trixie)\"\n  ```\n- `bundle/artifacts/vuln_marker_check.txt` — `docker exec` proof that the marker\n  file was written inside the vulnerable container.\n- `bundle/logs/fixed_container.log`, `bundle/artifacts/fixed_exploit.json` —\n  fixed build: `task_status=FAILED_WITH_TERMINAL_ERROR`,\n  `workflow_status=FAILED`, `rce_confirmed=false`, `task_output_result=null`.\n- `bundle/artifacts/fixed_marker_check.txt` — marker file absent in the fixed\n  container (`No such file or directory`).\n- Environment: Docker `--network host` on host `muramasa` (Arch Linux kernel);\n  sandbox reached the host-network conductor via the docker bridge gateway\n  `172.20.0.1:8080`; vulnerable image `conductoross/conductor:3.22.3`,\n  fixed image `conductoross/conductor:3.30.2`.\n\n## Recommendations / Next Steps\n\n- **Upgrade to Conductor ≥ 3.30.2** immediately. The `js`/`python` evaluators\n  no longer run with host access / host class loading enabled.\n- **Defense in depth:** put authentication/authorization in front of the\n  Conductor API (the community build is unauthenticated by default); run the\n  server as a non-root, least-privilege user; restrict which principals can\n  register/run workflows.\n- **Detection:** flag workflow definitions whose `INLINE`/`LAMBDA`/`DO_WHILE`/\n  `SWITCH` tasks contain `expression` strings referencing `getClass`,\n  `forName`, `Runtime`, `exec`, `ProcessBuilder`, or `java.` reflection; alert\n  on Conductor processes spawning shells.\n- **Testing:** add regression tests (as the fix did in `InlineTest.java`) that\n  assert a reflection-based RCE expression yields `FAILED_WITH_TERMINAL_ERROR`\n  for both `javascript` and `graaljs` evaluator types on every build.\n\n## Additional Notes\n\n- **Idempotency:** the script cleans up any leftover\n  `conductor-vuln-repro`/`conductor-fixed-repro` containers at startup and\n  stops/removes them after each phase, so it can be re-run cleanly. Each exploit\n  uses a timestamped workflow name and marker, so repeated runs do not collide.\n- **Networking:** in the DinD sandbox, Docker bridge port publishing (`-p`) does\n  not expose a reachable port to the sandbox container; the script therefore\n  uses `--network host` and reaches the conductor via the sandbox's default\n  gateway (`detect_target_ip`), falling back to `127.0.0.1` when the sandbox is\n  the docker host itself.\n- **Scope of the proof:** the in-range version demonstrated (3.22.3) uses the\n  plain `HostAccess.ALL` configuration; the 3.30.0/3.30.1 partial blocklist is\n  not separately bypassed here — the negative control uses the complete fix\n  (3.30.2) per the CVE's fixed version.\n","cve_id":"CVE-2026-58138","cwe_id":"CWE-94 Improper Control of Generation of Code (Code Injection)","source_url":"conductor-oss/conductor","reproduced_at":"2026-07-06T08:20:35.100146+00:00","duration_secs":1493.0,"tool_calls":258,"handoffs":2,"total_cost_usd":3.945370720000001,"agent_costs":{"judge":0.008198800000000001,"repro":1.87599918,"support":0.06348171,"vuln_variant":1.9976910299999997},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.008198800000000001},"repro":{"accounts/fireworks/routers/glm-5p2-fast":1.87599918},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.06348171},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.9976910299999997}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:21:01.389300+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12074,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":11459,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9823,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":12186,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":18395,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1352,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":2753,"category":"other"},{"path":"bundle/repro/exploit.py","filename":"exploit.py","size":7202,"category":"script"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":2249,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1166,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1115,"category":"other"},{"path":"bundle/logs/vuln_container.log","filename":"vuln_container.log","size":24890,"category":"log"},{"path":"bundle/logs/vuln_exploit.log","filename":"vuln_exploit.log","size":480,"category":"log"},{"path":"bundle/logs/fixed_container.log","filename":"fixed_container.log","size":28344,"category":"log"},{"path":"bundle/logs/fixed_exploit.log","filename":"fixed_exploit.log","size":340,"category":"log"},{"path":"bundle/logs/vuln_variant_steps.log","filename":"vuln_variant_steps.log","size":16395,"category":"log"},{"path":"bundle/logs/vuln_variant/fixed_version.txt","filename":"fixed_version.txt","size":225,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":7996,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":5016,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3218,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":2533,"category":"other"},{"path":"bundle/logs/vuln_variant_A.log","filename":"vuln_variant_A.log","size":345,"category":"log"},{"path":"bundle/logs/vuln_variant_B.log","filename":"vuln_variant_B.log","size":336,"category":"log"},{"path":"bundle/logs/vuln_variant_C.log","filename":"vuln_variant_C.log","size":347,"category":"log"},{"path":"bundle/logs/variant_vuln_container.log","filename":"variant_vuln_container.log","size":24887,"category":"log"},{"path":"bundle/logs/variant_fixed_container.log","filename":"variant_fixed_container.log","size":28343,"category":"log"},{"path":"bundle/logs/vuln_variant/vuln_version.txt","filename":"vuln_version.txt","size":60,"category":"other"}]}