Tuesday, January 9, 2007

Discovering a Java Application's Security Requirements

Application security is not easy - we know that. But we also know there are steps we can take to mitigate the risk of security failures. If we are network engineers, we invest in knowledge of network partitioning and packet filters. If we program in C, we protect against buffer overflow. If we program in Java, we consider running our application under the protection of a security manager. In each case, we use knowledge of best practices to give ourselves an advantage over inadvertent system failure.

The security provisions for Java applications are well-documented, and form a superset of the discussion in this article. Our discussion focuses on aspects of Java security managers, a topic which is a small subset of the Java security architecture.

A security manager is a class that is or extends java.lang.SecurityManager, and which checks certain application actions for permissibility during runtime. Once under the control of a security manager, the application can only perform actions that are specifically allowed by an associated security policy. By default that policy is specified in a plaintext policy file. The actions in question include writing files to specific directories, writing system properties, and making network connections to specific hosts, to name only a few. It takes no more than a simple JVM command line option to force a Java application to run under a security manager, and the policy file is easily created with any text editor.

While editing such a security policy file and adding the various rules of interest is not difficult, getting the policy right can be more challenging. And although no one can get that policy right for us, we can use tools to help us understand what that policy should be. Developing and using such a tool is what we are about to undertake. And once we have the broad, fine-grained policy it discovers, we can use it as a starting point in developing a production runtime policy or simply study it to better understand and appreciate our application's security needs.

The core code of this article is a custom security manager whose use requires Sun's JSE 5 JVM. The JSE 5 requirement derives from the security manager's dependence on the Java Reflection API to acquire private member data from a particular Java system class. Use of the Reflection API is required because of a lack of public access to certain private member data which the manager needs to function. The consequence of using reflection of private members is that the manager becomes strongly bound to the JVM internals on which it runs. This is not a serious consequence, however, as the manager is a development tool, not a production component. Once the manager has suggested a policy starting point, we can take that policy and run our application subject to it on any modern JVM.

The Default Java Security Manager

The vast majority of Java code we see written, discussed, and invoked in the contemporary literature represents the application that does not run subject to a security manager. Such applications therefore have full access to all machine resources, including disk, network, and application shutdown. Such broad access is easily restricted, however. An application can be forced to run under the default Java security manager simply by setting the -Djava.security.manager option on the JVM command line.

Consider this simple application, designed to read and print the user's home directory

public class PrintHome {
public static void main(String[] argv) {
System.out.println(System.getProperty("user.home"));
}
}

Compile the code, and run it subject to the default security manager

$ cd $HOME/Projects/CustomSecurityManager

$ javac PrintHome.java

$ java -Djava.security.manager PrintHome
Exception in thread "main" java.security.AccessControlException:
access denied (java.util.PropertyPermission user.home read)
...
at PrintHome.main(PrintHome.java:5)

whereupon the application fails to acquire and print the user.home property, and where we have omitted most of the stacktrace for readability. The application fails to execute because the default security manager running with the default security policy forbids accessing the property user.home. This privilege must be granted explicitly in a runtime policy file.

Creating a policy file policy.txt containing the single rule

grant codeBase "file:/home/cid/Projects/CustomSecurityManager/"{
permission java.util.PropertyPermission "user.home", "read";
};

and rerunning the application with a reference to that policy file solves the problem of read access to user.home:

$ java -Djava.security.policy=policy.txt -Djava.security.manager PrintHome
/home/cid

Note that we refer the JVM to the policy file by setting the system property java.security.policy=policy.txt. We also assume the class PrintHome resides in the directory /home/cid/Projects/CustomSecurityManager. The rule in the policy.txt file permits any code contained in a file in that directory to read system property user.home. As a result, the rule allows PrintHome to run as intended. The file in which code is contained is termed a codebase. A codebase is therefore a class or jar file.

A Security Policy Profiler: ProfilingSecurityManager

As we mentioned earlier, creating a security policy file by hand is not difficult, even though utilities like policytool are available to assist. And there are syntax shortcuts allowed in the policy file that are quite powerful, allowing the creation of very expressive, efficient rules. Using such advanced rules notation, we can specify, for example, codebase URLs that refer recursively to entire directory trees. While such recursive URL specification is quite useful and convenient, it can also mask to the human reader the true, fine-grained depth of the resource needs of the application. It is precisely this detail that we seek.

So our goal is two-fold: Firstly we are interested in running our application subject to a security policy, or at the very least we want to determine the security needs of that application. Secondly, we want a programmatic method of determining those needs.

With those goals in mind, we introduce the custom security manager secmgr.ProfilingSecurityManager. This class extends java.lang.SecurityManager, but does not enforce a security policy in the sense discussed thus far. Instead, it reports what that security policy would be if the application were granted access to everything it requests at runtime. We can then take that reportage and cast it into a starting point for a runtime security policy. Thus, both our goals are satisfied.

To use ProfilingSecurityManager, we first compile and strategically place it by itself in its own jar file (the source code can be found in the resources section). Placing ProfilingSecurityManager alone in its own jar file will allow us to filter and suppress outputting rules that arise from actions originating in its own jar file codebase. ProfilingSecurityManager is aware of its own unique codebase via

if( url.toString().equals(thisCodeSourceURLString) ) {
return null;
}

and can therefore prevent reporting on itself. Compile and jar the tool:

$ mkdir -p classes lib

$ rm -rf classes/* lib/*

$ javac -d classes ProfilingSecurityManager.java

$ jar cf lib/psm.jar -C classes secmgr/manager

$ rm -rf classes/secmgr/manager

Before we proceed too much further, we should say something about how to activate ProfilingSecurityManager as our application's security manager. Recall earlier, we forced our applications to run subject to the default Java security manager by setting the system property -Djava.security.manager specifically without a corresponding property value. We need to take this one step further and specify the custom security manager as the security manager by assigning a value to the system property: -Djava.security.manager=secmgr.ProfingSecurityManager. Thus activated, ProfilingSecurityManager will write to System.out the rules needed in a policy file that will allow the application to run without throwing security violation exceptions. However, these rules cannot be processed into a final, useable form until the application has completed its run under ProfilingSecurityManager. Why? Because only then is it known that the application has finished requesting access to checked resources. So for processing and tidying the rules when the application has finished running under ProfilingSecurityManager, we provide a simple Perl script parsecodebase.pl (also in the sample code) to aggregate, format, and output the rules in a readable format, sorted and grouped by codebase.

OK, so running the simple PrintHome application with ProfilingSecurityManager specified as the security manager and with the rules output processed by parsecodebase.pl produces

$ java -cp .:lib/psm.jar -Djava.security.manager=secmgr.ProfilingSecurityManager PrintHome > raw.out

$ parsecodebase.pl < raw.out > policy.txt

$ cat policy.txt
grant codeBase "file:/home/cid/Projects/CustomSecurityManager/" {
permission java.util.PropertyPermission "user.home", "read";
};

$ java -cp . -Djava.security.manager -Djava.security.policy=policy.txt PrintHome
/home/cid

So we see again the ProfilingSecurityManager satisfies both our design goals:

* Our application runs under a security manager with a well-defined application-specific policy
* We determined that policy programmatically

How does ProfilingSecurityManager work? ProfilingSecurityManager overrides both versions of java.lang.SecurityManager's checkPermission method. The two forms of this method are the central chokepoints for examining to which resource or action the application requested access. The overridden checkPermission methods always return without throwing exceptions -- essentially meaning "access allowed" -- but not until they build and output the rule to allow the action responsible for their being called in the first place.

A More Complex Example: Profiling a Tomcat Web Application

With that simple example behind us, let us examine a nontrivial use of ProfilingSecurityManager: profiling a Tomcat web application. Tomcat can be made to run under the default Java security manager by passing the -security option to the standard startup script:

$ $CATALINA_HOME/bin/startup.sh -security

Passing the -security option to startup.sh leads to the calling of $CATALINA_HOME/bin/catalina.sh with the same -security option. $CATALINA_HOME/bin/catalina.sh is the script that actually calls java to run the Tomcat bootstrap class org.apache.catalina.startup.Bootstrap, and furthermore in this case subject to the default policy specified in $CATALINA_HOME/conf/catalina.policy. If we leave the invocation at that Tomcat will run under the default Java security manager subject to the default shipped policy. But we need to do a bit more work to profile Tomcat and the webapps it may contain. To profile the webapp using ProfilingSecurityManager, we must develop a new Tomcat startup script. The new startup script is a temporary device, and will be used only for profiling, then discarded.

Make a backup copy of $CATALINA_HOME/bin/catalina.sh. Insert the shell command set -x in $CATALINA_HOME/bin/catalina.sh near the top of the script, and start Tomcat. Save the displayed shell executed command to a file that will hold the temporary startup script. Stop Tomcat, and edit the temporary script, specifying ProfilingSecurityManager as the security manager and modifying the classpath to locate it.

Under Tomcat 5.5.17 under Linux, here is what the temporary startup script looks like before we edit it for purposes of using ProfilingSecurityManager, with a bit of tweaking and formatting

#!/bin/sh
log=$CATALINA_HOME/logs/catalina.out

/java/jdk/jdk1.5.0_06/bin/java \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
-Djava.util.logging.config.file=/home/tomcat/tomcat/conf/logging.properties \
-Djava.endorsed.dirs=/home/tomcat/tomcat/common/endorsed \
-classpath :/home/tomcat/tomcat/bin/bootstrap.jar:\
/home/tomcat/tomcat/bin/commons-logging-api.jar \
-Djava.security.manager \
-Djava.security.policy==/home/tomcat/tomcat/conf/catalina.policy \
-Dcatalina.base=/home/tomcat/tomcat \
-Dcatalina.home=/home/tomcat/tomcat \
-Djava.io.tmpdir=/home/tomcat/tomcat/temp \
org.apache.catalina.startup.Bootstrap start >> $log \
2>&1 &

After editing for purposes of using ProfilingSecurityManager, the startup script looks like this:

#!/bin/sh
log=$CATALINA_HOME/logs/catalina.out
PATHTOPSM=$HOME/lib/psm.jar # make sure the profiler jar file is here

/java/jdk/jdk1.5.0_06/bin/java \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
-Djava.util.logging.config.file=/home/tomcat/tomcat/conf/logging.properties \
-Djava.endorsed.dirs=/home/tomcat/tomcat/common/endorsed \
-classpath $PATHTOPSM:/home/tomcat/tomcat/bin/bootstrap.jar:\
/home/tomcat/tomcat/bin/commons-logging-api.jar \
-Djava.security.manager=secmgr.manager.ProfilingSecurityManager \
-Djava.security.policy==/home/tomcat/tomcat/conf/catalina.policy \
-Dcatalina.base=/home/tomcat/tomcat \
-Dcatalina.home=/home/tomcat/tomcat \
-Djava.io.tmpdir=/home/tomcat/tomcat/temp \
org.apache.catalina.startup.Bootstrap start >> $log \
2>&1 &

The two scripts differ in that the new temporary version

1. augments the classpath to point to where the ProfilingSecurityManager class resides, namely in $HOME/lib/psm.jar
2. specifies ProfilingSecurityManager as the security manager in the -Djava.security.manager parameter

Now we can start profiling. Start Tomcat with the temporary startup script. Put the web applications of interest through their paces by forcing them to cover code that would be covered in a production scenario. Code-covering the web applications is admittedly a tall order, and may only be partially achievable. Stop Tomcat, and run $CATALINA_HOME/logs/catalina.out through parsecodebase.pl, as shown below, with the processed rules saved to policy.txt.

$ parsecodebase.pl < $CATALINA_HOME/logs/catalina.out > policy.txt

Listing 9. Processing the rules generated by Tomcat and any executed web applications contained by it.

Be aware that ProfilingSecurityManager can only generate rules for code executed during profiling. It does not examine bytecode in class files, probing all code branches that can in theory be reached during an arbitrary runtime instance. Such bytecode analysis could be an area of future work, and would complement, but not replace, the runtime analysis done by ProfilingSecurityManager.

An examination of the policy file $CATALINA_HOME/conf/catalina.policy that ships with Tomcat reveals that Tomcat ("Catalina") system codebases are granted all platform permissions. ProfilingSecurityManager, in fact, will discover these same rules for Tomcat system classes, but will specify them in a fine-grain fashion. The rules ProfilingSecurityManager discovers about Tomcat system classes should be manually pruned from policy.txt.

The rules that remain in policy.txt after pruning Tomcat system rules are the starting point for our production security policy. These rules represent our web application's security requirements. Each of these rules should be examined carefully to understand what it does and to confirm that it is consistent with our application's goals. When we are confident we have a good draft policy, make a backup copy of $CATALINA_HOME/conf/catalina.policy, and incorporate the new draft rules from policy.txt into it. Then revert back to the original Tomcat startup script with the -security option set, and continue testing.

Conclusion

Running our applications under a Java security manager can increase the robustness of our code. And while getting the security policy right will be challenging, doing so provides peace of mind that our code is running subject to security constraints we prescribe. ProfilingSecurityManager can help us get that policy right by giving us full visibility into the set of resources to which the application has requested access.