Building a bigger sandbox

Security in JDK 1.2 gives developers more control. Find out how to take advantage of this flexible new model

T he sandbox. Discussions of Java's security model always seem to involve mention of the so-called sandbox security model. This article won't delve deeply into this topic; instead, we'll discuss how new additions to the Java API in the 1.2 release let us expand the original sandbox idea, create our own sandboxes, and even eliminate the sandbox entirely.

The security manager and the sandbox

Java has always had many different faces to its security model. It has a strongly typed compiler to eliminate programming bugs and help enforce language semantics, a bytecode verifier that makes sure the rules of Java are followed in compiled code, a classloader that's responsible for finding, loading, and defining classes and running the verifier on them, and the security manager -- the main interface between the system itself and Java code.

We'll be concentrating on the age-old security manager and the new addition to the JDK, the access controller. To refresh our memories, the security manager in Java is composed of a series of checkXXX methods that we can override, defining the logic we desire. In JDK 1.1, this logic either disallows the request (by throwing a java.lang.SecurityException) or allows the request (either via some ornate logic scheme or by simply returning).

Security managers exist to enforce the rules of the sandbox. The sandbox concept is fairly simple: When you run a piece of Java code, you may want the sandbox to provide an area for the code to do what it needs to do. But in many situations, you need to restrict the bounds of this area. Code that is trusted resides outside the sandbox; code that is untrusted is confined within it. Trusted code is the code in the Java API and code loaded from the classpath. Untrusted code is code loaded from outside the classpath, usually from the network. So Java applications, by default, live outside the sandbox and Java applets, by default, are confined within it.

The need for this "play area" is obvious when you think of an applet. Suppose a site called www.badpeople.com decided to create an applet that ostensibly showed slides of Sunday cartoons. You might enjoy that. But behind the scenes, while you were lazily chuckling at the sideshow, the applet would really be scouring your hard drive for private information. Without the sandbox, this scenario is all too possible. The sandbox represents the limits a program has put upon it. The sandbox is another term for a security policy.

As Java grows, the need for varying security policies increases. It is agreed by almost everyone that the original sandbox model of JDK 1.0.x, though safe, was too restrictive. With JDK 1.1 the addition of digital signatures allowed expansion of the original sandbox policy. If the user trusted the digitally signed code, users could allow normally untrusted code to access resources. (Though their discussion is beyond the scope of this article, digital signatures still play an important role in JDK 1.2.)

The sandbox, though, is just the security policy -- the equivalent of a law. And a policy or law must be enforced to be effective. The government can pass a law in your town tomorrow outlawing red shoes, but if nobody enforces it, it's not much of a law. The sandbox won't effectively confine code and its behavior without an enforcement mechanism. This is where the security manager comes into the picture. Security managers make sure all restricted code stays in the sandbox.

Here's an example of how to create a security manager in JDK 1.1 that allows reading files, but disallows writing files:

public class MySecurityManager extends java.lang.SecurityManager {

public void checkRead(String file) throws SecurityException { // reading is allowed, so just return

return; }

public void checkWrite(String file) throws SecurityException { // writing is not allowed, so throw the exception

throw new SecurityException("Writing is not allowed"); } } // end MySecurityManager

The MySecurityManager class does its job, but in a purely binary fashion: Either you can perform the requested action or you cannot. Often it is desirable to maintain a little more granularity in the system. Perhaps you want to permit reading, but only from files ending in txt. This might be achieved if we change the previous code as follows:

public class MySecurityManager2 extends java.lang.SecurityManager {

public void checkRead(String file) throws SecurityException {

//check the file extension to see if it ends in ".txt"

int index=file.lastIndexOf('.'); String result=file.substring(index, file.length()); if(result.equalsIgnoreCase(".txt")){ return; }else{ throw new SecurityException("Cannot read file: "+file); } } } // end MySecurityManager2

This version of the code offers a little more fine-tuned control than the original. With work, we could create a security manager that had very a granular security policy. This format -- subclassing java.lang.SecurityManager and overriding the appropriate methods -- was the only way security was controlled prior to JDK 1.2. This system offers many advantages:

  • It is very easy to provide a binary security model (yes, you can or no, you can't)

  • The methods in the SecurityManager class are called for you by the Java API; there's no need for you to call the code at all

  • The interface of this system is constant across all JVM platforms; one security manager can run everywhere

  • The additional size of a simple security manager is negligible

  • The security manager and class loader work hand-in-hand to ensure neither is compromised by accident or an act of evil

The traditional model of Java security falls short, however, when it comes to flexibility and intricate granularity. Its disadvantages as follows:

  • The control of security is in the developer's hands, not in a security specialist's hands

  • It's not easy to provide a customizable security model that varies from user to user

  • The only way to change an existing policy is to change or subclass the existing security manager; not all users have the capability of programming in Java

  • New security policies (non-system resource policies, for example) are difficult to implement

These disadvantages are erased with the security architecture of JDK 1.2 while the advantages remain.

Enter the access controller

Our discussion so far indicates that the traditional sandbox model, though useful, is simply not flexible enough for most systems. What may be a good policy today may not be appropriate tomorrow. A user could need his access level changed over time, not an easy thing to accomplish with the traditional security model. To provide greater control to both the developer and the end user, the java.security package was expanded to include classes like AccessController, Permission, and Policy. We'll discuss the Permission and Policy objects a bit later.

The AccessController is the muscle of the security manager. The sandbox, remember, is the policy in effect. The security manager enforces this policy by making use of the AccessController. In JDK 1.2, the SecurityManager class has the same interface as prior releases (allowing for backward compatibility), but the logic has changed. Now each checkXXX method makes a call to a static method inside the AccessController class. When you call the checkRead(String file) method now, it defers the work to the AccessController.

Let's assume a user, Grover, wants to access a file called joe.rec. Compile-time isn't the best time to set security boundaries, as it isn't yet known who that user will be. It makes more sense to discover whether or not Grover can access the file at runtime. Someone, probably a security administrator, could create a policy file containing Grover's permissions. The flow of this process is given in the diagram below.

Security Flow Diagram

I've simplified the procedure here; the code samples aren't literally from the source code, but the functionality is exactly the same. The important thing to understand here is that the SecurityManager is no longer solely responsible for the logic of the desired security policy. In fact, it could be said that the SecurityManager is no longer needed in the JDK. There are a few reasons it has remained part of the system, but the main one is to ensure that code written with previous versions of the JDK will still function. Remember that the methods inside the SecurityManager are just that: methods. You can perform any logic you choose inside them. If you're using a Java program from an earlier release of the JDK and there is a security manager installed for it, the code does not have to be changed to still work. The original logic will apply for all the methods you overrode and the AccessController will be called for the rest.

Now we'll look at how the AccessController works with user-defined policies.

The access controller, permissions, and policies

If you look at the above diagram, you'll see that when the SecurityManager's checkRead(String file) method is called, it creates a FilePermission object. It then passes this FilePermission object to the AccessController, which compares it to the current Policy object for acceptance or denial.

Whew!

The good news is, all of this calling of methods and passing of objects is handled for you by the Java API. Though there are times when you may wish to create your own Permission objects or call the AccessController yourself, we'll leave discussion of that for a future article. What we need to define now are the system-provided permissions we wish to allow a program to have and the policy file those permissions will be stored in.

A permission in Java represents a specific access privilege. The abstract java.security.Permission class is subclassed to create specific permission types. To represent a file access permission, the java.io.FilePermission class is used. To represent access to a property, the java.util.PropertyPermission class is used. Socket access is encapsulated in the java.net.SocketPermission class and so on.

Let's take a look at the FilePermission class. As a demonstration of this whole security system, we'll write a Java app that is allowed to read any file in the /tmp directory. The code for the application is called FileApp.java. When we run this code, the doit method is called and an attempt is made to create a FileInputStream. This attempt results in a check-in with the SecurityManager to see if one is loaded. If one is loaded, it will call the AccessController and check whether or not read access to the specified file is allowed. If you run this code directly, as normal, you'll get no errors. That's because Java applications are still run without any sandbox limitations at all. To force the system to load a security manager and enforce the permissions defined, you have to pass the -usepolicy flag to the interpreter. (Note: In JDK 1.2 beta 3, to use the -usepolicy flag, you also must pass the -new flag.)

    java -new -usepolicy FileApp test.txt

This -usepolicy flag tells the virtual machine to make use of the policy files on the system. In the /lib/security directory of your Java installation, you'll find a file called java.security. This is a list of properties that are used for security. You'll find a section toward the middle of the file that contains these lines:

# The default is to have a single systemwide policy file, 
# and a policy file in the user's home directory.
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy

You may specify as many policy files as you wish in the java.security file, but they must be numbered sequentially. The first one listed (policy.url.1) specifies the system security policy, java.policy. If you look closely at this file, you'll see that it's a very limited sandbox. If an application makes use of only this policy file, it has no access to system resources. Notice, for example, that there are no permissions granted for reading files. That's right! An application that uses this default security policy is as restrained as the normal applet.

The next policy entry in the java.security file is the .java.policy file (notice the leading period [.]). This is the default name given the user security policy. By default, this policy file is stored in the user's home directory. More policy files can be added sequentially, perhaps related to a specific application. The virtual machine will create a java.security.Policy object from all these defined files. It forms a union of the files, so all the policies in .java.policy are first, then the user-specific policy, then the third file, and so on, until there are no more policy files to read. Since policy files only grant privileges, there is no danger of clashing. In other words, there is no provision for denying a privilege except to simply not grant it.

A policy file entry is composed of one or more grant entries. A grant entry has the form:

grant [signedBy >signer<] [, codeBase >url of code source<] {
    permission >permission class< [>name< [, >actions<] ];
    permission >permission class< [>name< [, >actions<] ];
    permission >permission class< [>name< [, >actions<] ];
    ...
}
1 2 Page 1
Page 1 of 2