You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The initial goal is to provide sandboxing of compilation and evaluation of groovy rules.
But this requires preliminary improvements to the groovy rules system.
Raw recommandations from Codex (5.5 xhigh) to use as guidelines for further defining tasks in this epic.
I reviewed the Groovy rule path. Short version: sandboxing is currently not implemented. The repo has the sandbox instrumentation dependency and transformer wired in, but no active policy is registered when rules run.
What Is In Place
groovy-sandbox is present via container/build.gradle and versioned as org.craftercms:groovy-sandbox:4.3.2 with Groovy 5.0.6 in gradle.properties.
manager/src/main/java/org/openremote/manager/rules/RulesetDeployment.java:70 defines GroovyDenyAllFilter, but it is never registered.
manager/src/main/java/org/openremote/manager/rules/RulesetDeployment.java:92 creates a static GroovyShell with new SandboxTransformer().
manager/src/main/java/org/openremote/manager/rules/RulesetDeployment.java:322 still has // TODO Implement sandbox.
manager/src/main/java/org/openremote/manager/rules/RulesResourceImpl.java:277 restricts Groovy creation/update/delete to super users for realm and asset rules. That is access control, not sandboxing.
The important subtlety: SandboxTransformer rewrites Groovy calls so they can be intercepted, but without a registered GroovyInterceptor those calls are allowed. The sandbox library’s interceptor is thread-local, so even
registering it around script.run() would not protect when/then closures later executed by Easy Rules unless rule firing is also wrapped.
Current Execution Model
manager/src/main/java/org/openremote/manager/rules/RulesetDeployment.java:324 parses the Groovy script.
manager/src/main/java/org/openremote/manager/rules/RulesBuilder.java:75 stores Groovy closures for conditions/actions.
manager/src/main/java/org/openremote/manager/rules/RulesEngine.java:565 later executes those closures via Easy Rules.
So sandboxing needs to cover both deployment-time script execution and runtime rule evaluation/actions.
Recommended Next Steps
Define the policy first, before implementation:
Decide whether sandboxed Groovy should be suitable for non-super users. If yes, treat it as hostile code. If no, keep the super-user gate and treat sandboxing as damage reduction.
Replace GroovyDenyAllFilter with a real deny-by-default GroovyInterceptor:
Allow exact receiver/type/member signatures only. Do not allow broad classes like Object, Class, DefaultGroovyMethods, Logger, File, URL, Runtime, System, Thread, reflection, classloaders, process APIs, JDBC, network clients,
or metaClass.
Register the interceptor around every sandboxed execution:
Wrap both:
script.run() in compileRulesGroovy
engine.fire(...) or the condition/action execution path
Use try/finally to unregister. Because the CrafterCMS fork uses a thread-local single interceptor, do not assume registration during compilation applies to scheduled rule execution threads.
Add a SecureASTCustomizer compile-time gate:
Groovy’s own docs recommend allowlists because otherwise future language features are implicitly permitted, and if no allow/disallow list is configured everything is permitted. Use it to restrict imports, static imports,
packages, annotations, class/method definitions, tokens, statements, and receiver classes. Source: Groovy SecureASTCustomizer docs: https://docs.groovy-lang.org/latest/html/api/org/codehaus/groovy/control/customizers/SecureASTCustomizer.html
Disable compile-time escape hatches:
Pay particular attention to @grab, global/local AST transforms, annotations, script class generation, static initializers, method definitions, and custom classes. Runtime interception does not protect code that executes during
compilation or AST transformation.
Stop using one static GroovyShell:
The existing comment already documents classloader breakage with redeployed scripts defining classes. Use a per-deployment GroovyShell/GroovyClassLoader, dispose it on undeploy, and avoid sharing rule-defined classes through
facts. Prefer Java model classes for shared data.
Harden exposed facades before loosening the REST gate:
AssetsFacade and UsersFacade contain visible scope restrictions. But manager/src/main/java/org/openremote/manager/rules/facade/HistoricFacade.java:44 and manager/src/main/java/org/openremote/manager/rules/facade/
PredictedFacade.java:44 do not visibly enforce ruleset scope before querying/updating datapoints. manager/src/main/java/org/openremote/manager/rules/facade/WebhooksFacade.java:47 can build a target from an arbitrary webhook
URL, which needs SSRF controls.
Add resource isolation:
A Groovy sandbox cannot reliably stop while(true), huge allocations, sleeps, blocked I/O if allowed, or CPU/memory exhaustion. Java SecurityManager is not a good answer on Java 21; it has been deprecated for removal since Java
17. Source: OpenJDK JEP 411: https://openjdk.org/jeps/411
Gotchas To Watch
Registering the current deny-all filter would break legitimate rules immediately.
Registering only during compilation/deployment leaves runtime closures unsandboxed.
Allowing getClass, .class, Class.forName, metaClass, method pointers, or broad Groovy extension methods is usually enough to escape.
Allowing Logger directly is risky; expose a tiny rule logger wrapper instead.
Allowing Webhooks without URL policy enables internal network access.
Rule facts are shared at engine level, so one ruleset can affect other rulesets in the same engine.
Loop detection only counts triggered rules; it does not interrupt a single closure that never returns.
Existing Groovy examples use imports, packages, streams, Optional, collections, query objects, and closures, so the initial allowlist will need compatibility tests against the current rule corpus.
I would implement this in phases: first keep Groovy super-user-only and add tests proving obvious escapes are blocked; then harden facades and resource limits; only then consider enabling sandboxed Groovy for non-super users.
The practical target is: super-user Groovy remains available, but sandboxing narrows the blast radius of mistakes, compromised accounts, copied snippets, or overly broad integrations.
I’d do it in this order:
Inventory current Groovy rule usage
Scan real customer rules and test/demo rules for imports, constructors, method calls, static calls, and facade usage. Treat that as the compatibility contract.
Turn that into explicit facades
For every legitimate pattern, prefer a facade method over allowing raw JVM/Groovy access. Examples: asset lookup/update, fact matching, notifications, alarms, datapoints, webhooks. The sandbox allowlist should mostly contain
OpenRemote facade APIs, simple value objects, collections, streams, optionals, predicates/closures, and query builders.
Harden facades before relying on them
AssetsFacade and UsersFacade already encode scope. Datapoint and predicted datapoint facades should also enforce ruleset scope. Webhooks need URL policy/SSRF protection. Returned objects should be immutable or narrow DTOs
where possible.
Add sandbox tests before enforcing broadly
Add tests for both “works” and “blocked” cases:
System.exit, Runtime.exec, new File, URL.openConnection, Class.forName, .class, getClass, classLoader, reflection, metaClass, @grab, static imports, arbitrary constructors, direct field access, thread creation, sleeps/loops
where possible.
Add compile-time and runtime gates
Use SecureASTCustomizer to reject dangerous syntax/imports/annotations early. Use groovy-sandbox runtime interception around both script.run() and rule firing, because the closures execute later.
Keep a migration mode
Initially run sandbox in “report-only” or behind a config flag for super-user Groovy. Log denied signatures and missing facade capabilities. Once customer rule coverage is good, flip to enforcement.
One thing I would be strict about: do not make the runtime allowlist broad just to keep old scripts working. If a real rule needs something, add a small facade method with scope checks. That gives you an audit trail and avoids
accidentally allowing the entire JVM through Object, Class, Groovy extension methods, or model internals.
Activity
ebariaux commentedon Jun 3, 2026
Raw recommandations from Codex (5.5 xhigh) to use as guidelines for further defining tasks in this epic.
I reviewed the Groovy rule path. Short version: sandboxing is currently not implemented. The repo has the sandbox instrumentation dependency and transformer wired in, but no active policy is registered when rules run.
What Is In Place
The important subtlety: SandboxTransformer rewrites Groovy calls so they can be intercepted, but without a registered GroovyInterceptor those calls are allowed. The sandbox library’s interceptor is thread-local, so even
registering it around script.run() would not protect when/then closures later executed by Easy Rules unless rule firing is also wrapped.
Current Execution Model
So sandboxing needs to cover both deployment-time script execution and runtime rule evaluation/actions.
Recommended Next Steps
Define the policy first, before implementation:
Decide whether sandboxed Groovy should be suitable for non-super users. If yes, treat it as hostile code. If no, keep the super-user gate and treat sandboxing as damage reduction.
Replace GroovyDenyAllFilter with a real deny-by-default GroovyInterceptor:
Allow exact receiver/type/member signatures only. Do not allow broad classes like Object, Class, DefaultGroovyMethods, Logger, File, URL, Runtime, System, Thread, reflection, classloaders, process APIs, JDBC, network clients,
or metaClass.
Register the interceptor around every sandboxed execution:
Wrap both:
Use try/finally to unregister. Because the CrafterCMS fork uses a thread-local single interceptor, do not assume registration during compilation applies to scheduled rule execution threads.
Add a SecureASTCustomizer compile-time gate:
Groovy’s own docs recommend allowlists because otherwise future language features are implicitly permitted, and if no allow/disallow list is configured everything is permitted. Use it to restrict imports, static imports,
packages, annotations, class/method definitions, tokens, statements, and receiver classes. Source: Groovy SecureASTCustomizer docs:
https://docs.groovy-lang.org/latest/html/api/org/codehaus/groovy/control/customizers/SecureASTCustomizer.html
Disable compile-time escape hatches:
Pay particular attention to @grab, global/local AST transforms, annotations, script class generation, static initializers, method definitions, and custom classes. Runtime interception does not protect code that executes during
compilation or AST transformation.
Stop using one static GroovyShell:
The existing comment already documents classloader breakage with redeployed scripts defining classes. Use a per-deployment GroovyShell/GroovyClassLoader, dispose it on undeploy, and avoid sharing rule-defined classes through
facts. Prefer Java model classes for shared data.
Harden exposed facades before loosening the REST gate:
AssetsFacade and UsersFacade contain visible scope restrictions. But manager/src/main/java/org/openremote/manager/rules/facade/HistoricFacade.java:44 and manager/src/main/java/org/openremote/manager/rules/facade/
PredictedFacade.java:44 do not visibly enforce ruleset scope before querying/updating datapoints. manager/src/main/java/org/openremote/manager/rules/facade/WebhooksFacade.java:47 can build a target from an arbitrary webhook
URL, which needs SSRF controls.
Add resource isolation:
A Groovy sandbox cannot reliably stop while(true), huge allocations, sleeps, blocked I/O if allowed, or CPU/memory exhaustion. Java SecurityManager is not a good answer on Java 21; it has been deprecated for removal since Java
17. Source: OpenJDK JEP 411: https://openjdk.org/jeps/411
Gotchas To Watch
I would implement this in phases: first keep Groovy super-user-only and add tests proving obvious escapes are blocked; then harden facades and resource limits; only then consider enabling sandboxed Groovy for non-super users.
The practical target is: super-user Groovy remains available, but sandboxing narrows the blast radius of mistakes, compromised accounts, copied snippets, or overly broad integrations.
I’d do it in this order:
Inventory current Groovy rule usage
Scan real customer rules and test/demo rules for imports, constructors, method calls, static calls, and facade usage. Treat that as the compatibility contract.
Turn that into explicit facades
For every legitimate pattern, prefer a facade method over allowing raw JVM/Groovy access. Examples: asset lookup/update, fact matching, notifications, alarms, datapoints, webhooks. The sandbox allowlist should mostly contain
OpenRemote facade APIs, simple value objects, collections, streams, optionals, predicates/closures, and query builders.
Harden facades before relying on them
AssetsFacade and UsersFacade already encode scope. Datapoint and predicted datapoint facades should also enforce ruleset scope. Webhooks need URL policy/SSRF protection. Returned objects should be immutable or narrow DTOs
where possible.
Add sandbox tests before enforcing broadly
Add tests for both “works” and “blocked” cases:
System.exit, Runtime.exec, new File, URL.openConnection, Class.forName, .class, getClass, classLoader, reflection, metaClass, @grab, static imports, arbitrary constructors, direct field access, thread creation, sleeps/loops
where possible.
Add compile-time and runtime gates
Use SecureASTCustomizer to reject dangerous syntax/imports/annotations early. Use groovy-sandbox runtime interception around both script.run() and rule firing, because the closures execute later.
Keep a migration mode
Initially run sandbox in “report-only” or behind a config flag for super-user Groovy. Log denied signatures and missing facade capabilities. Once customer rule coverage is good, flip to enforcement.
One thing I would be strict about: do not make the runtime allowlist broad just to keep old scripts working. If a real rule needs something, add a small facade method with scope checks. That gives you an audit trail and avoids
accidentally allowing the entire JVM through Object, Class, Groovy extension methods, or model internals.