Sandboxes in Corda 5 — Java Security Manager

March 09, 2023

By: Miljenko Brkic, Principal Software Engineer at R3

This article is a follow-up to the Corda 5 Sandboxes blog post. It will explore in more detail how Corda 5 uses Java Security Manager to secure sandboxes.

Corda 5 supports multiple virtual nodes sharing a single Corda installation and multiple CorDapps (Corda distributed applications) running within the same Corda (JVM) process. This introduces the risk of one CorDapp interacting with another or with the Corda platform. That interaction could be unintended, like a simple dependency clash, but it could also be malicious. Sandbox is an execution environment within a JVM process that provides isolation for a CorDapp. It shields it from outside threats but it also restricts what it can do so that running potentially dangerous code cannot harm others.

Tractor in sandbox

What is Java Security Manager?

Java Security Manager protects applications against threats posed by running untrusted code. It was originally designed to protect users from potentially malicious Java applets downloaded from websites, which was similar to the threat that web browsers face today running JavaScript. While CorDapp writers trust their own code, they can’t trust CorDapps written by others, so Corda, as an application hosting platform, needs to treat CorDapps as untrusted code that could be malicious.

Security Manager acts as a gatekeeper, controlling access to sensitive resources (for example, accessing your local disk or local network) and ensuring that malicious code cannot compromise the security of the system. It enables administrators to define and enforce security policies within the JVM hosting Java applications. Administrators can specify exactly which resources an application can access and which actions it’s permitted to perform. For example, an application might be granted permission to read a file, but not to write to it or delete it.

Security Manager key concepts

Security Manager permits access to sensitive resources. It is not enabled by default; instead, it has to be explicitly enabled (for example, using the −Djava.security.manager option). There can be only one global instance of Security Manager and it’s accessible using System.getSecurityManager(). When some running code calls a method on the Java API, requesting an operation be performed, the JVM will check with the security manager if this is allowed. An example of this would be the ability for a Java application to open an external connection over HTTP (such as connecting to a web host). If the administrator of the JVM has denied that ability, the application will receive an error. This introduces a separation between what an application is allowed to do at runtime vs at compile time, meaning an application may work within a less constrained environment and fail in a more constrained one.

Access controller provides the basis of the default implementation of the Security manager.

Code source is the location from which the class was obtained (for example, a web URL or JAR file).

Permission represents access to a protected resource.

Protection domain encapsulates the characteristics of a domain, which encloses a code source and a set of permissions granted to it.

Security policy provides management of permissions in a configurable way. The default implementation uses text files, where each text block in a policy file defines a Protection Domain:

grant signedBy "r3", codeBase "http://www.r3.com/" {                // Code Source
    permission java.io.FilePermission "/tmp", "read";               // Permissions
    permission java.net.SocketPermission "*:1024−", "connect";
};

How Security Manager checks permissions

Let’s take an example and see how Security Manager checks permissions. Consider a CorDapp that wants to read a file from a filesystem:

// CorDapp

public readFile(String fileName) {
    InputStream inStream = new FileInputStream(fileName);
    ...
}

It does this by executing the readFile() method that creates a Java API’s FileInputStream for reading. The constructor of FileInputStream will first check if there is a Security Manager; if there is, it will call its checkReadmethod:

// FileInputStream

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    ...
}

Security Manager will then delegate permission checking to method checkPermission of the Access Controller:

// SecurityManager

public void checkRead(String file) {
    checkPermission(new FilePermission(file,
        SecurityConstants.FILE_READ_ACTION));
}

public void checkPermission(Permission perm) {
    java.security.AccessController.checkPermission(perm);
}

The diagram below shows all these methods on the call stack (on the left) and also related Protection Domains (on the right):

Diagram showing all methods on the call stack (on the left) and also related Protection Domains (on the right).

In order to determine whether this sensitive operation is allowed, Access Controller collects all Protection Domains related to the classes on the call stack. The operation is allowed only if all protection domains have the permission to perform it; otherwise, a SecurityException is thrown. System Domain has all permissions and permissions of other domains are defined via Security Policy.

There is an exception to this, which will be explained using another example. As an application platform, Corda also provides a number of injectable services to CorDapps. One of them is the JSON Marshalling Service, which depends on a third-party library that uses reflection. This means that a CorDapp should have reflection permission in order to use it. However, we want untrusted CorDapps to be able to use trusted platform services without the need to have special permissions.

That can be accomplished with a privileged caller that can perform a sensitive operation on behalf of class that doesn’t have the required permission. Access Controller provides the doPrivileged() method and when this method is found on the stack, it stops the further checking of protection domains.

The snippet below shows how the Corda JSON Marshalling Service uses doPrivileged() to enable CorDapps to serialize classes to JSON, without the need to have the required reflection permissions:

// JsonMarshallingServiceImpl

override fun format(data: Any): String {
      return try {
          AccessController.doPrivileged(PrivilegedExceptionAction {
              mapper.writeValueAsString(data)
          })
      } catch (e: PrivilegedActionException) {
          throw e.exception
      }
  }

Security Manager and OSGi

CorDapps running within sandboxes need to be isolated from each other. Classes used in specific CorDapps shouldn’t be visible to others. This is achieved using the OSGi (Open Service Gateway Initiative), which is a framework for developing and deploying modular Java applications. A unit of modularization” is called a bundle and is a Java archive file (JAR) that additionally contains a manifest that describes it and its dependencies.

The OSGi Security Layer is based on Java security architecture. In OSGi, Protection Domain is mapped to a bundle. A bundle’s permissions are handled through Conditional Permission Admin. OSGi adds several features to the Java security, including:

  • OSGi specific permissions
  • Conditional permission management
  • Deny-access decisions

The Corda Security Manager

Corda’s Security Manager adds permissions to Conditional Permissions Admin’s permission table using the bundle location as a condition. Bundles loaded for the specific sandbox type share the same location prefix, so this enables the application of different sets of permissions to different types of sandboxes.

Disabling access to internal platform classes

CorDapps have access to platform services via interfaces. Granting them reflection permissions could lead to various malicious exploits of platform classes that implement those services and compromise a sandbox. However, a CorDapp may depend on some public libraries that use reflection, and unfortunately, most public libraries don’t use reflection in a privileged call. That means that it’s not enough to grant reflection permissions only to the library: the CorDapp needs it as well. So the desired solution is to allow reflection but not over Corda internal packages.

That goal is achieved by allowing reflection but denying access to internal packages by using the accessClassInPackage permission. The Security Manager will check this permission only if a package name starts with a prefix defined for a security property package.access. This property is defined in the java.security file distributed with JVM (which can be replaced or overridden with a custom file) and can also be set with Security.setProperty().

Configuring security permissions

One of the best security practices is the principle of least privilege. It is a security concept of providing no more permissions than is necessary to perform the required job. Corda follows this principle, which means that CorDapps won’t have any permission granted by default.

However, some CorDapps might need certain permissions granted in order to do their job; for example, make HTTP requests to external services. Configuring permissions can be complex and requires a good understanding of Java security concepts. It’s also a very delicate task since granting permissions lowers sandbox security. For this reason, Corda security permissions are managed by Corda administrators via the configuration of security policy.

Corda security policies

Corda comes with a few predefined security profiles that can be used as provided or customized for specific needs. The strictest policy is applied by default, but a Corda administrator can override this policy if required.

Policies can have “allow” and “deny” access blocks. Each block starts with a condition that needs to be satisfied in order to apply that block. After that is a list of permissions that are either “allowed” or “denied” based on the block type.

Snippet below shows one deny-access block for Flow sandbox:

DENY {
[org.osgi.service.condpermadmin.BundleLocationCondition "FLOW/*"]
(java.io.FilePermission "<>" "read,write,delete,execute,readLink")
(java.lang.RuntimePermission "getFileSystemAttributes" "")
(java.lang.RuntimePermission "readFileDescriptor" "")
(java.lang.RuntimePermission "writeFileDescriptor" "")
(java.net.SocketPermission "*:1−" "accept,listen,connect,resolve")
(java.net.URLPermission "http://*:*" "*:*")
(java.net.URLPermission "https://*:*" "*:*")
(java.lang.RuntimePermission "accessDeclaredMembers" "")
(java.lang.reflect.ReflectPermission "suppressAccessChecks" "")
(java.lang.reflect.ReflectPermission "newProxyInPackage.*" "")
...
} "High security profile for FLOW Sandbox"

Summary

This article provided more details about Security Manager and how it is used in Corda 5 to securely run user code within sandboxes. Security of sandboxes also relies on OSGi that is used to load bundles and control visibility between them. We will follow up with another article about how we use OSGi in Corda 5.

Share: