# Patch Analysis — CVE-2026-58138

## Overview

CVE-2026-58138 is an unauthenticated RCE in Orkes/Conductor-OSS caused by
GraalVM polyglot script evaluators running with full host access. The fix
lands in release **3.30.2** via two commits:

| Commit | Date | Title | Release |
|--------|------|-------|---------|
| `87a7d96aabbb706d6e84f812b93da5165028d18f` | 2026-05-06 | Deny access to some classes in js evaluator, deny host access in python evaluator (#1057) | 3.30.0/3.30.1 (partial) |
| `c691e35e768caeb802c9f06ecdd9674c80081af1` | 2026-05-22 | Restrict graaljs further (#1123) | 3.30.2 (complete) |

The v3.30.2 tag resolves to commit `2bea5078f916c0d529cd0f32a55f79425b65a5b8`.

## What the Fix Changes

### 1. `ScriptEvaluator.createNewContext()` (JS evaluator — all JS paths)

**Before (v3.22.3):**
```java
private static Context createNewContext() {
    return Context.newBuilder("js")
            .allowHostAccess(HostAccess.ALL)
            .option("engine.WarnInterpreterOnly", "false")
            .build();
}
```

**After (v3.30.2, commit c691e35):**
```java
private static Context createNewContext() {
    HostAccess hostAccess =
            HostAccess.newBuilder(HostAccess.ALL)
                    .denyAccess(Class.class)
                    .denyAccess(ClassLoader.class)
                    .denyAccess(java.lang.reflect.Method.class)
                    .denyAccess(java.lang.reflect.Field.class)
                    .denyAccess(java.lang.reflect.Constructor.class)
                    .denyAccess(java.lang.reflect.Array.class)
                    .denyAccess(Runtime.class)
                    .denyAccess(ProcessBuilder.class)
                    .denyAccess(Process.class)
                    .denyAccess(System.class)
                    .denyAccess(Thread.class)
                    .denyAccess(ThreadGroup.class)
                    .build();
    return Context.newBuilder("js")
            .engine(ENGINE)                        // shared Engine w/ js.load=false
            .allowHostAccess(hostAccess)            // deny-list based
            .allowHostClassLoading(false)           // blocks Java.type()
            .allowNativeAccess(false)
            .allowCreateThread(false)
            .allowCreateProcess(false)              // blocks process creation
            .allowIO(IOAccess.NONE)                 // blocks file/network IO
            .allowEnvironmentAccess(EnvironmentAccess.NONE)
            .build();
}
```

Additionally, the shared `Engine` is built with:
```java
Engine.newBuilder("js")
    .allowExperimentalOptions(true)
    .option("engine.WarnInterpreterOnly", "false")
    .option("js.load", "false")     // disable JS module loading
    .option("js.print", "false")
    .option("js.console", "false")
    .build();
```

### 2. `PythonEvaluator.evaluate()` (Python evaluator)

**Before (v3.22.3):**
```java
try (Context context = Context.newBuilder("python").allowAllAccess(true).build()) {
```

**After (v3.30.2):**
```java
try (Context context = Context.newBuilder("python").build()) {
```

The `allowAllAccess(true)` master switch is removed. GraalVM defaults to safe
mode (HostAccess.NONE, no host class loading, no process creation, no IO).

## What the Fix Assumes

1. **Single JS context builder**: The fix assumes ALL JavaScript evaluation
   paths go through `ScriptEvaluator.createNewContext()`. This assumption is
   **correct** — source analysis confirms there are exactly two GraalVM
   `Context.newBuilder(...)` call sites in the entire codebase:
   - `ScriptEvaluator.createNewContext()` (JS)
   - `PythonEvaluator.evaluate()` (Python)

2. **All JS entry points funnel through ScriptEvaluator**: The fix assumes
   that `JavascriptEvaluator`, `GraalJSEvaluator`, `Lambda`, `DoWhile`,
   `DecisionTaskMapper`, `DefaultEventProcessor`, and
   `WorkflowTaskTypeConstraint` all call `ScriptEvaluator.eval()` or
   `evalBool()`. This assumption is **correct** — verified by grep of all
   `ScriptEvaluator` references in `core/src/main/`.

3. **deny-list + binary restrictions are sufficient**: The JS fix uses
   `HostAccess.newBuilder(HostAccess.ALL).denyAccess(...)` (deny-list) backed
   by binary restrictions (`allowHostClassLoading(false)`,
   `allowCreateProcess(false)`, `allowIO(IOAccess.NONE)`). Even if the
   deny-list misses a dangerous class, the binary restrictions prevent loading
   new classes or creating processes.

4. **Python safe defaults are sufficient**: The Python fix relies entirely on
   GraalVM's safe defaults after removing `allowAllAccess(true)`. No explicit
   deny-list or binary restrictions are added to the Python context.

## What the Fix Covers

All confirmed entry points to the vulnerable sinks:

| Entry Point | Sink | Covered? |
|-------------|------|----------|
| INLINE (evaluatorType=javascript) | ScriptEvaluator.createNewContext() | ✅ |
| INLINE (evaluatorType=graaljs) | ScriptEvaluator.createNewContext() | ✅ |
| INLINE (evaluatorType=python) | PythonEvaluator | ✅ |
| LAMBDA (scriptExpression) | ScriptEvaluator.createNewContext() | ✅ |
| DO_WHILE (loopCondition) | ScriptEvaluator.evalBool() | ✅ |
| DECISION (caseExpression, deprecated) | ScriptEvaluator.eval() | ✅ |
| Event handler condition (DefaultEventProcessor) | ScriptEvaluator.evalBool() / evaluators | ✅ |
| WorkflowTaskTypeConstraint validation | ScriptEvaluator.eval() | ✅ |

## What the Fix Does NOT Cover (Gaps Analysis)

### No gaps found for RCE bypass

The fix is **complete** for preventing RCE through the GraalVM script
evaluators:

1. **Host class loading disabled** (`allowHostClassLoading(false)`): Prevents
   `Java.type()` from loading any Java class, which would be needed to reach
   `Runtime`/`ProcessBuilder` without reflection.

2. **Process creation disabled** (`allowCreateProcess(false)`): Even if a host
   object with exec capability were reachable, GraalVM blocks process creation
   at the engine level.

3. **IO disabled** (`allowIO(IOAccess.NONE)`): Prevents file read/write and
   network access from the script context.

4. **Reflection denied** (`denyAccess(Class, Method, Field, Constructor,
   Array)`): Prevents the original exploit's technique of bootstrapping
   reflection from the bound `$` Map object.

5. **Deny-list may miss classes** (e.g., `java.lang.ProcessImpl`,
   `javax.script.ScriptEngineManager`), but this is mitigated by
   `allowHostClassLoading(false)` + `allowCreateProcess(false)`.

### Potential hardening recommendations

Although no bypass was found, the following would strengthen the fix further:

1. **Use `HostAccess.NONE` instead of `HostAccess.ALL` + deny-list for the JS
   evaluator**: The current approach starts with ALL access and removes
   dangerous classes. A safer approach would start with NO access and
   explicitly allow only what's needed (the bound `$` input object's data
   access). The Python evaluator already does this correctly (defaults to
   `HostAccess.NONE`).

2. **Add explicit restrictions to the Python evaluator**: While GraalVM's
   defaults are safe, explicitly setting `allowHostClassLoading(false)`,
   `allowCreateProcess(false)`, `allowIO(IOAccess.NONE)` on the Python context
   would provide defense-in-depth matching the JS evaluator's restrictions.

3. **Add a script deny-list / allow-list**: Consider restricting which
   JavaScript/Python APIs are available (e.g., only allow pure computation,
   no `Java.type`, no `import java`, no `process` module).

## Target Threat Model

The target's `SECURITY.md` (`conductor-oss/conductor`) contains only a
vulnerability reporting policy and supported versions table. There are **no
exclusions** or "this is not a vulnerability" disclaimers. The unauthenticated
API + inline script RCE is clearly in-scope as a security vulnerability.

The community Conductor REST API has **no authentication by default**, which
is the key trust boundary: a remote attacker who can reach the API can
register and execute workflows without credentials.
