Secure Groovy Script Execution in a Sandbox

Charles Chan
Level Up Coding
Published in
5 min readMar 18, 2021

--

Image by author composed from http://groovy-lang.org and https://www.compart.com/en/unicode/U+1F512

I was looking into executing untrusted Groovy Script in a sandbox. I was never a security guy, so the process was a valuable learning experience. I will use this article to summarize my findings.

I am working as a consultant at Capco.com. Many thanks to my colleagues who are generous with their comments. The opinions expressed here do not necessarily reflect those of the employer.

Java Sandbox

At a high level, a Java sandbox relies on the following three components working together:

  1. Bytecode verifier
  2. ClassLoader
  3. Security Manager and its policies

The bytecode verifier ensures that the compiled class files are valid and do not exploit the virtual machine. Class loaders are created hierarchically. If you imagine a tree of class loaders, the “leaf” class loaders can only access their classes and their parents’ classes on the same branch. Using this structure, you can execute code in a restricted environment by using a custom class loader. Finally, a security manager provides runtime permission checks based on a custom policy file.

Project Requirements

As everyone knows, there are tradeoffs between convenience and security. Finding the right balance requires discussions and collaborations with all key stakeholders. Before we dive into the article, let us assume that these are the facts we are dealing with:

  1. Today, Groovy scripts are written for a very specific context. There are hundreds of them. They are executed in the same VM as the main program and they have access to classes from the main program. In other words, they are trusted.
  2. Going forward, that specific context may be opened up to untrusted parties. We need a plan to secure the environment to run untrusted Groovy script in the future.

Given these facts, we have decided to focus on runtime permission checks and optionally source level checks using a Security Manager and various Groovy compiler customizations.

Steps to Secure the Groovy Script Execution environment

Step 1: Security Manager and Policy File

First and foremost, we need to install a security manager to perform runtime permission checks. You don’t want a Groovy script to invoke System.exit(0) if it is running in the same VM. A security manager works in tandem with a policy file. A policy file declares the permissions granted to each codebase. Codebase is basically a URL that describes where the code is loaded from. Fortunately, all Groovy script interpreted by the Groovy interpreter has the file:/groovy/script codebase or the file:/groovy/shell codebase. So, a policy file that grants all permission to your running program but not Groovy would look like this:

grant codeBase "file://<your jar file>" {
permission java.security.AllPermission;
};
grant codeBase "file:/groovy/shell" {
};
grant codeBase "file:/groovy/script" {
};

Once we have the policy file, we can apply it by adding the following command line arguments:

-Djava.security.manager -Djava.security.policy=<policy file>

Step 2: Managing Policy Violations

If you are the lucky few in a green field development, there is no need to worry about this. But if you are, like me, trying to secure an existing environment, there are bound to be exceptions. The existing Groovy scripts may require permissions that are not granted in the policy file.

Instead of changing the policy file to grant the missing permissions, I suggest you make use of the AccessController.doPrivileged call. This method executes the block of code in a “privileged” manner, which means no permission checks. It is important that this block of code be as small as possible. The reason I advocate for this approach is that a method call is easily identifiable and can therefore be rectified in the near future.

Next, we will look at the specific measures that Groovy provides to help secure script execution.

Step 3: Groovy Binding Restrictions

Groovy Binding is a mechanism to pass variables into a Groovy script. If the Groovy script is untrusted, we must control what can be passed into the Groovy script. This is especially true because not all 3rd party library APIs are protected by permission checks.

Step 4: Apply Groovy Script Compiler Configurations

A Groovy script compiler can also be customized by different compilation customizers. For example, an ImportCustomizer can restrict what a Groovy script can import and a SecureASTCustomizer attempts to limit what construct a Groovy script can use. Unfortunately, both of them can be workaround quite easily as described in this article: https://kohsuke.org/2012/04/27/groovy-secureastcustomizer-is-harmful/ (by Kohsuke Kawaguchi from Jenkins). This is a good article to read to understand the limitation of these customizer.

In the end, Mr. Kawaguchi is kind enough to publish the Groovy sandbox project on Github: https://github.com/jenkinsci/groovy-sandbox and the Script Security Plugin (Jenkins specific): https://github.com/jenkinsci/script-security-plugin. These two projects are definitely worth looking into for possible integration into your project.

Other Considerations

Besides the above considerations, you need to also manage how much CPU and memory can a Groovy script consume. If this is your concern, you can execute the Groovy script in a different thread than your main program. You can use a monitoring thread to kill the Groovy script thread if necessary.

Running your Groovy script in a separate container can also provide further protection but it is more costly to operate and cannot be easily rectified into an existing implementation.

Final Thoughts

Executing untrusted code is by definition a dangerous proposition. Your options to secure the execution environment may be limited by an existing implementation. No two projects are the same. By summarizing my findings here, I hope you can also benefit from them.

PS

I learned many things during this exercise. It is hard to incorporate them into the article’s flow. So, I will list them here instead:

  1. The Java Security Manager is a System level setting. To control permission checks in a per-thread basis, you must write your own Security Manager and use ThreadLocal to conditionalize the permission checks.
  2. Similarly, a custom Security Manager can use together with a custom class loader to provide more flexibility.
  3. Java access control is based on codebase, i.e. where the code is loaded from. I found that running a Groovy script inside a JUnit test do not have the file:/groovy/shell or the file:/groovy/script codebase. Instead, they inherit the JUnit’s class’s code base. I have spent endless frustrating hours debugging something that should have worked.
  4. Similarly, during development, a Spring Boot application running with the Maven Spring Boot Plugin has a different code base. You should grant permissions to both: ${user.dir}/target/classes and ${user.home}/.m2/repository
  5. Policy can be added programmatically as well as using a policy file.

--

--

A seasoned consultant specialized in Software Development and Architecture. Charles also loves to tavel, follow him on https://www.gorestrepeat.com/