Scripting and sandboxing in JVM

It’s a very common situation when an application needs some runtime customization, but it’s impossible via settings and you are too lazy to change source code, or you just cannot update your application too often. In that case you may use different script engines in order to change application behavior in runtime. Moreover, the developer doesn’t have to do changes themself. It may be the duty of administrators, analysts, etc

 

So, I want to describe the way of one java application, from the simplest script engine up to engines with secured sandboxes.

 

The first approach – javascript engine

We just needed to preprocess input strings – just filter them, trim and normalize. There was nothing interesting there. In this case a javascript engine was the most suitable solution. So  out choise was Nashorn.  With this interpreter you are able to:

  • Make some code on javascript of course
  • Implement your own javascript functions
  • Access to Java classes
  • Invoke Java methods from javascript
  • Invoke javascript methods from Java
  • Implement a sandbox
  • etc

It’s a part of JDK and there is even a console interpreter.

jjs
jjs> print (‘giggity’)
giggity

Integration is also very simple:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval("print('giggity');");

You may find more examples here or here. This solution was acceptable for that time, but requirements had changed over time

The next step – Groovy engine

The code base was growing and we needed something more serious than a JavaScript engine. In addition, the whole team is JVM-based, so the next choice was the Groovy engine.

Binding binding = new Binding();
GroovyShell shell = new GroovyShell(binding);
Script scrpt = shell.parse(new File("app.groovy"));
binding.setVariable("foo", "bar");
shell.evaluate("println(foo)")

So we had the full power of a Java Virtual Machine. Since that point our application was going to production usage. A little bit about the app – it contains a huge amount of very sensitive data. Also, there are complex ACLs, rules and so on, which are protecting data from unauthorized access. In this stage we were faced with big security issues. As I mentioned before, we had the full power of JVM and user-script had access to reflection API, internal classes, bean, processes and so on. The first thing that we tried was the sandbox by Kohsuke Kawaguchi. For the first try it was ok, but it works only as a whitelist model. It became incompatible with our requirements.

Below were the final requirements for our Groovy sandbox:

  • Writing arbitrary groovy code
  • Using arbitrary groovy libraries
  • Using arbitrary Java libraries
  • Prevent data leaking
  • Prevent DoS from user script
  • Allow some multi-threading

The typical userscript workflow

  1. Write some code
  2. Run it in the sandbox
  3. If there are error – goto 1
  4. Send it for the final review
  5. If review is not ok – goto 1
  6. Publish it and use it in production

The main purpose of user scripts is text processing.

Now, let’s walk through the points above

Writing arbitrary Groovy code

Everything is fine here. Do whatever you want

Using arbitrary Groovy libraries

Implemented by loading different scripts (just scripts, not jars or something like that) from kind of online repository (simple concatenation).

Using arbitrary Java libraries

This situation is more complex than the previous. There isn’t any remote class or jar loading. All classes are located just in classpath. New libraries must be reviewed and added in the app manually via build system. It’s not as flexible as in the case with groovy libraries, but it is easier to implement, enough and also meets the requirement.

Prevent data leaking

This is the most interesting problem. In case of a mistake – all the system turns into one big RCE. In order to reduce the probability of RCE, protection system is built like bulkhead on a ship, because successful attacks often consist of multiple steps.

vulnerabilities

If one level is broken, the next must protect the app from exploiting. A good example is here.

The first step – moving the script engine outside the main application. In case of any vulnerability in the engine or incorrect SecurityManager settings, you can easily gain access to internal services, database connection, etc. (e.g., via Reflection).

groovy sandbox before

Before

groovy sandbox step 1

After

 

The second step – moving the new application to another host or container. It’s obvious, but nobody must gain access to important resources such as config files, private keys and so on, even when something goes wrong. So, within this step, a firewall must be configured to refuse any connections to another host, except for main application connections and some maintenance stuff.

groovy sandbox step 2

 

The third step – configuring SecurityManager. It’s a class, which allows implementing a custom security policy. You are able to restrict or allow some sensitive operations by configuring SecurityManager. Checking methods in SecurityManager look like checkSomething(java.security.Permission permission). There are a few types of permissions – FilePermission, SocketPermission, RuntimePermission, etc. In order to allow something, you must build policy file or override SecurityManager and provide your own implementation. You can find below some additional information.

All classes in the application are divided into two groups – signed and unsigned. Signed classes are classes of sandbox application while unsigned classes are groovy scripts. It needs for separation trusted code with all permission from untrusted code which is executed in our Groovy sandbox.

groovy sandbox step 3

 

How to build signed jar:

Create a keystore and generate private key

keytool -genkey -keyalg RSA -alias sandbox -keystore /pass/to/keystore.jks -storepass <<password>>

Now it’s time to sign your jar.

You can use jarsigner

jarsigner -keystore /path/to/keystore sandbox.jar sandbox

Or use Jarsigner Plugin

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jarsigner-plugin</artifactId>
            <version>3.0.0</version>
            <executions>
                <execution>
                    <id>sign</id>
                    <goals>
                        <goal>sign</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <keystore>/path/to/keystore.jks</keystore>
                <alias>sandbox</alias>
                <storepass>storepass</storepass>
                <keypass>keypass</keypass>
            </configuration>
        </plugin>
    </plugins>
</build>

Then use signer plugin to sign your jar. When using Spring Boot plugin with executable jar or war, it might need to shadow classes using shadow plugin instead of Spring Boot plugin

<build>
    <plugins>
        <plugin>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
                <minimizeJar>false</minimizeJar>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.handlers</resource>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.schemas</resource>
                    </transformer>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.factories</resource>
                    </transformer>
                </transformers>
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Final policy file looks like

keystore "file:///path/to/keystore.jks", "JKS";

keystorePasswordURL "file:///path/to/file/with/keystore/password.txt";

grant signedBy "sandbox {
  permission java.security.AllPermission;
};

grant {
     permission java.net.SocketPermission "main-app-host:port "connect,resolve";
};

SocketPermission is needed for access to the main application’s API, which is protected by internal authorization and authentication mechanisms.

Now we are able to invoke the main-app-API using binding

@Autowired
private ApiClient apiClient;

.....

Binding binding = new Binding();
binding.setVariable("apiClient", apiClient);
Object res = new GroovyShell(binding).evaluate(script);

groovy sandbox step 4

 

Prevent DoS of main application from user script

It’s pretty easy perform DoS attack from userscript by running something like this

while(true){
  apiClient.callSomethingSlow();
}

Measures undertaken on the main app side are described below:

  • Amount of simultaneously executing scripts is limited by settings
  • The main app API is protected by ratelimiter
  • All methods, which return big amount of data have paging with page size limit

But as for the sandbox-app side, there is a problem. The app is protected by SecurityManager, but it isn’t almighty. SecurityManager has the ability to prevent thread modification, but user may start their own thread from Groovy:

Thread.start {
  while(true)
}

To deal with that, you can

  • Try to use  CompilationCustomizers like SecureASTCustomizer and restrict using some classes but it may conflict with initial requirements
  • Try to implement custom ClassLoader and disable loading for certain classes

We decided to choose another way – try to figure out hanging threads and stop them.

First, we need to separate threads run via script engine from other threads. This is done by using ThreadGroup.

Even if a new thread is started from script, the thread will have the same ThreadGroup. But ThreadGroup has a constructor with parent ThreadGroup. In other words, there is an ability to create new ThreadGroup with parent like main or system. In order to avoid that, let’s make a trick.

  • When script is starting – create a ThreadGroup and add it to a collection
  • When ThreadGroup(name, parent) is calling, there is a SecurityManager.checkAccess(ThreadGroup) invocation. So we can just restrict access to system thread groups from script.

Next, use deprecated ThreadGroup.stop method. It stops all threads in current ThreadGroup and also stops children groups.

Here is an explanation why you must not use this:

Stopping a thread causes it to unlock all the monitors that it has locked. (The monitors are unlocked as the ThreadDeath exception propagates up the stack.) If any of the objects previously protected by these monitors were in an inconsistent state, other threads may now view these objects in an inconsistent state. Such objects are said to be damaged. When threads operate on damaged objects, arbitrary behavior can result.

We don’t care if objects are damaged or not, because, in case of timeout, we just throw an exception, without fetching any result. And of course, there isn’t any way to control execution flow inside userscript, so we have no choice.

But it’s not only that. If there are some thread execution pool (e.g., cached) which is created inside Groovy sandbox, it’s impossible to stop threads because stopping thread can start a new one.

groovy sandbox threads

For that kind of problem, a possible solution is using an external guard application. This app fetches active script threads and when hanging threads are detected, the guard app just restarts sandbox app or send a notification to the person responsible.

Putting it all together (simplified)

public class SandboxSecurityManager extends SecurityManager {

    private final Set<ThreadGroup> threadGroups = new HashSet<>();

    public void addThreadGroup(ThreadGroup threadGroup) {
        threadGroups.add(threadGroup);
    }

    public void removeThreadGroup(ThreadGroup threadGroup) {
        threadGroups.remove(threadGroup);
    }

    @Override
    public void checkAccess(ThreadGroup g) {
        if (g != Thread.currentThread().getThreadGroup() 
                && threadGroups.contains(Thread.currentThread().getThreadGroup())) { 
            //throw an exception when current thread is started from script,
            //but trying to access to another TG
            throw new SecurityException("Access denied to ThreadGroup " + g.getName());
        }
        super.checkAccess(g);

    }
}

 

public class EngineService {


    private final ApiClient apiClient;

    .....


    public Object run(Script script) throws Exception {

        Worker worker = new Worker(script, apiClient);
        ThreadGroup threadGroup = new ThreadGroup("script-" + UUID.randomUUID());
        Thread t = new Thread(threadGroup, worker);
        SandboxSecurityManager securityManager = (SandboxSecurityManager) System.getSecurityManager();
        securityManager.addThreadGroup(threadGroup);

        t.start();

        if(!waitForThreadCompleted(threadGroup, Duration.ofSeconds(TIMEOUT))){
            threadGroup.stop();
            if(hasActiveThreads(threadGroup)){
                sendToGuard(threadGroup);
            }
            throw new TimeoutException();
        }

        return worker.result;
    }


    public class Worker implements Runnable {

        private volatile Object result;
        private final Script script;
        private final ApiClient apiClient;

        public Worker(Script script, ApiClient apiClient) {
            this.script = script;
            this.apiClient = apiClient;
        }

        private GroovyShell createShell() {

            Binding binding = new Binding();
            binding.setVariable("apiClient", apiClient);
            return new GroovyShell(binding);
        }

        public void run() {
            result = createShell().evaluate(script.getScript());
        }

    }

    ....

}

groovy sandbox final step

Conclusion

Of course, there are some unsolved problems in this application; for instance, you can just copy-paste sensitive information. But this can be resolved via in-house security policies. And in most cases, you don’t need to build this paranoid Groovy sandbox, just use one of AST, especially if it’s possible to determine and limit all necessary classes and operations. Keep your script engines in a safe place; otherwise, it turns into the most dangerous vulnerability.

Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
agle
agle
2 months ago

It’s very well written. Can you put the complete code(EngineService.java) on GitHub?

2
0
Would love your thoughts, please comment.x
()
x
Back to top