GitHub Advisoryadvisory · Maven

GHSA-7mqr-33rv-p3mp Expression Injection in OpenRemote

CRITICAL Published 2026-04-14 Modified 2026-04-24 Official page ↗
Aliases
CVE-2026-39842
Ecosystem
Maven
Packages
io.openremote:openremote-manager
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Weaknesses
CWE-917, CWE-94
Reviewed
2026-04-14

Affected packages

• Maven io.openremote:openremote-manager: < 1.22.0 → fixed in 1.22.0

Description

Summary The OpenRemote IoT platform's rules engine contains two interrelated critical expression injection vulnerabilities that allow an attacker to execute arbitrary code on the server, ultimately achieving full server compromise. • Unsandboxed Nashorn JavaScript Engine: JavaScript rules are executed via Nashorn's ScriptEngine.eval() with user-supplied script content and no sandboxing, class filtering, or access restrictions. Critically, any non-superuser with the write:rules role can create JavaScript rulesets. • Inactive Groovy Sandbox: The Groovy rules engine has a GroovyDenyAllFilter security filter that is defined but never registered (the registration code is commented out), rendering the SandboxTransformer ineffective. While Groovy rules are restricted to superusers, the absence of sandboxing violates the principle of defense in depth. Details Attacker-Controllable Source-to-Sink Paths There are two non-superuser and two superuser exploitable attack paths from the REST API entry point to final code execution. (JavaScript × Realm/Asset + Groovy × Realm/Asset) The most critical path is detailed below. Path 1: Unsandboxed JavaScript Expression Injection via Realm Ruleset (Non-Superuser Exploitable) `RulesetDeployment.java` L368: ```java Object result = scriptEngine.eval(script, engineScope); ``` The Nashorn JavaScript engine is initialized without a `ClassFilter`, allowing `Java.type()` to access any JVM class — including `java.lang.Runtime` (for RCE), `java.io.FileReader` (for file read), and `java.lang.System` (for env theft). `RulesResourceImpl.java` L309 (and L331, L359, L412, L437): ```java // VULNERABLE: only restricts Groovy, JavaScript completely unrestricted if (ruleset.getLang() == Ruleset.Lang.GROOVY && !isSuperUser()) { throw new WebApplicationException(Response.Status.FORBIDDEN); } ``` Non-superuser attackers with only the `write:rules` role can submit arbitrary JavaScript rules that execute with full JVM access. ``` ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ SOURCE → SINK Complete Data Flow │ ├─────────────────────────────────────────────────────────────────────────────────────────┤ │ │ │ ① HTTP REQUEST (Source / Entry Point) │ │ POST /api/{realm}/rules/realm │ │ Content-Type: application/json │ │ Body: { "type":"realm", "lang":"JAVASCRIPT", │ │ "rules":"", ... } ← Attacker-controlled malicious script │ │ │ │ ↓ JAX-RS Deserialization │ │ │ │ ② RulesResource.createRealmRuleset() │ │ model/.../rules/RulesResource.java:153-158 │ │ @POST @Path("realm") @RolesAllowed("write:rules") │ │ Interface: long createRealmRuleset(RequestParams, @Valid RealmRuleset ruleset) │ │ JSON body → RealmRuleset object (Jackson deserialization) │ │ RealmRuleset.rules field ← attacker's malicious script content │ │ RealmRuleset.lang field ← "JAVASCRIPT" │ │ │ │ ↓ Calls implementation │ │ │ │ ③ RulesResourceImpl.createRealmRuleset() ← ⚠️ Authorization flaw here │ │ manager/.../rules/RulesResourceImpl.java:250-267 │ │ - L255: isRealmActiveAndAccessible(realm) — checks realm accessible ✓ │ │ - L255: isRestrictedUser() — restricted users blocked ✓ │ │ - L262: if (ruleset.getLang() == Ruleset.Lang.GROOVY && !isSuperUser()) │ │ → Only blocks GROOVY for non-superusers ✓ │ │ - ⚠️ NO check for Lang.JAVASCRIPT! JavaScript rules pass through unrestricted ⚠️ │ │ - L265: ruleset = rulesetStorageService.merge(ruleset) │ │ → Passes the RealmRuleset (with malicious script) to persistence layer │ │ │ │ ↓ JPA Persistence │ │ │ │ ④ RulesetStorageService.merge() │ │ manager/.../rules/RulesetStorageService.java:155-159 │ │ - L157: entityManager.merge(ruleset) │ │ → Persists RealmRuleset entity (with rules and lang fields) to REALM_RULESET table│ │ - After JPA transaction commit, Hibernate event listener is triggered │ │ │ │ ↓ Hibernate Event → Camel Message │ │ │ │ ⑤ Hibernate Interceptor → PersistenceService Event Publishing │ │ container/.../persistence/PersistenceEventInterceptor.java:51-62, 102-119 │ │ - L55-56: new PersistenceEvent<>(CREATE, entity, propertyNames, state) │ │ → Hibernate Interceptor captures the JPA entity persist event │ │ - L102-119: afterTransactionBegin() registers Synchronization callback; │ │ afterCompletion() calls eventConsumer.accept(persistenceEvent) (L114) │ │ container/.../persistence/PersistenceService.java:596-607 │ │ - PersistenceService implements Consumer> (L84) │ │ - L596-606: accept() publishes event to Camel SEDA topic: │ │ producerTemplate.withBody(persistenceEvent) │ │ .withHeader(HEADER_ENTITY_TYPE, entity.getClass()) │ │ .to(PERSISTENCE_TOPIC).asyncSend() │ │ - PERSISTENCE_TOPIC = "seda://PersistenceTopic?multipleConsumers=true&..." │ │ (PersistenceService.java:209-210) │ │ │ │ ↓ Camel Route Consumption │ │ │ │ ⑥ RulesService.configure() — Camel Route Processor │ │ manager/.../rules/RulesService.java:228-235 │ │ - L228: from(PERSISTENCE_TOPIC) │ │ - L230: .filter(isPersistenceEventForEntityType(Ruleset.class)) │ │ → Filters for Ruleset-type events only │ │ - L232-234: .process(exchange -> { │ │ …

Related

Alias · CVE (1)

  • CVE-2026-39842 OpenRemote is an open-source IoT platform. vulnerability

Weakness · CWE (2)

  • CWE-917 Improper Neutralization of Special Elements used in an Expression Language Statement weakness
  • CWE-94 Improper Control of Generation of Code weakness

References