DEV Community

Edward Lynch-Milner
Edward Lynch-Milner

Posted on

Java DockerSandbox Library

GitHub: https://github.com/edwardUL99/DockerSandbox

I would like to introduce an open source library I worked on under the Apache 2.0 license called DockerSandbox. It provides a wrapper API to the Docker Java Client at https://github.com/docker-java/docker-java that allows code to be executed in sandbox containerized environments. It provides similar functionality for Java that StepikOrg provides for Python with epicbox at https://github.com/StepicOrg/epicbox.

It aims to provide a simple to use API for Java to try and achieve as little verbosity as possible. All that needs to be setup are Docker images that are capable of running the languages that wish to be sandboxed. This post outlines the initial stable release of the API and an example of how to use it.

Use Case

Suppose you wish to execute code that you do not trust that may be received from another source, for example, from a remote code submission server (like Replit). The server accepting this code could simply spin up a subprocess to execute the code. However this has security risks associated with it such as:

  • File deletion
  • Malicious code submitted
  • Malwares
  • etc.

While Docker is still not the safest in terms of code execution (you may go a virtual machine route, which still has vulnerabilities and much higher overhead), the library takes some precautions by instructing the setup of a user with the least privileges in the container to be used through profiles (more later), called sandbox. The network for the Docker containers can also be disabled.

This library allows you to spin up a Docker container per execution of the code so that even if the code did something malicious inside the container, the container is destroyed once the code has finished execution. It adds an abstraction layer to the Docker Java client library to make this process easier

Example

The following is an example of the library in use to compile a C program using a gcc_compile profile which compiles a program using root user and then runs the program using a gcc_run profile using the sandboxed used:

package io.github.edwardUL99.docker.sandbox;

import io.github.edwardUL99.docker.sandbox.api.Docker;
import io.github.edwardUL99.docker.sandbox.api.DockerSandbox;
import io.github.edwardUL99.docker.sandbox.api.components.Result;
import io.github.edwardUL99.docker.sandbox.api.components.WorkingDirectory;

public class Example {
    public static void main(String[] args) {
        DockerSandbox sandbox = DockerSandbox.builder()
                        .withJson("profiles.json") // see profiles.json in the root of the project for the example file
                // Or you can do withShellProfiles(Docker.Shell.SH or Docker.Shell.BASH, profiles)
                        .withBinding("/path/to/local:/path/to/remote")
                        .withEnvironmentVariables("VAR1=VALUE1", "VAR2=VALUE2")
                        .build();

        sandbox.start("/home/sandbox");

        try {
            Docker.Command command = new Docker.Command("gcc main.c -o main");
            Result result = sandbox.run("gcc_compile", command,
                    new WorkingDirectory.UploadedFile("main.c", "/path/to/main.c"));

            // do something with result

            command = new Docker.Command("./main");
            result = sandbox.run("gcc_run", command, "Stdin Input"); // notice how this run command uses the compiled file from the previous execution
            // but you don't have to re-upload it as generated files from the previous call
            // are shared

            // do something with result

            // the call to finish in the finally block will free any resources such as created files on the host machine in the working directory
        } catch (Exception ex) {
            ex.printStackTrace();
            sandbox.cleanup(); // clean up any created containers that didn't get removed
        } finally {
            sandbox.finish(); // ensure all resources are freed
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The call to sandbox.cleanup() ensures that if any exceptions are thrown, any dangling containers are cleared. sandbox.finish() is placed in the finally block to ensure the the container created is torn down. The gcc_run and gcc_compile profiles can be seen here:

{
   "name": "gcc_compile",
   "image": "gcc-docker",
   "container-name": "gcc-docker",
   "user": "root",
   "networkDisabled": true
},
{
   "name": "gcc_run",
   "image": "gcc-docker",
   "container-name": "gcc-docker",
   "user": "sandbox",
   "networkDisabled": true
}
Enter fullscreen mode Exit fullscreen mode

The call to sandbox.start("/home/sandbox") sets up a working directory mount on the sandbox so files created in the gcc_compile execution can be shared with the container created in the gcc_run execution. I.e., here, the compilation output is executed in the run container.

Setup

To setup the library, you need to define 1 or more Profiles. A profile contains the name of the profile (which is passed into the sandbox.run call, the name of the Docker image, a name to give to spun up containers, the user to run the command under and finally, to enable/disable network connectivity for the container.

These profiles can be defined in a JSON file of the following structure:

{
  "profiles": [
    ... profiles go here
  ],
  "docker_host": "unix:///var/run/docker.sock",
  "shell": "bash"
}
Enter fullscreen mode Exit fullscreen mode

You also configure the host of the Docker engine and the shell to execute on the Docker containers. On configuration, you pass the path to the JSON file into the sandbox builder. A sample profiles.json and sample images are provided in the GitHub repository.

The profiles and shell can also be configured programatically like so:

dockerSandbox = DockerSandbox.builder()
                .withShellProfiles(Docker.Shell.BASH, new Profile("java_run", "java-docker", "java-docker", "sandbox"),
                        new Profile("java_compile", "java-docker", "java-docker", "root"),
                        new Profile("gcc_run", "gcc-docker", "gcc-docker", "sandbox"),
                        new Profile("gcc_compile", "gcc-docker", "gcc-docker", "root"))
                .withEnvironmentVariables("VAR1=VALUE1", "VAR2=VALUE2", "VAR3=VALUE3")
                .build();
Enter fullscreen mode Exit fullscreen mode

Download

You can download the library using Maven at the following coordinates:

<artifactId>io.github.edwardUL99</artifactId>
<groupId>docker-sandbox</groupId>
<version>1.0.0</version>
Enter fullscreen mode Exit fullscreen mode

Thank You

Thank you for taking the time to read this post. I hope the library can be of some use to somebody. If you have any feature requests or find any bugs, they can be reported on the GitHub repository :)

Discussion (0)