The main purpose of this article is to shed some light on a major change that was recently made to the JDK 1.2 API for privileged code blocks. We begin by explaining the evolution of Java’s security model from a “sandbox” architecture to a trust model. We also briefly touch on the notion of stack inspection, which is the way JDK 1.2 actually makes access control decisions behind the scenes. With these two things under our belt, we’ll be ready to take on the new API.
The evolution of Java securityThere are two major approaches to addressing the security concerns raised by mobile code systems: sandboxing and code signing. The first of these approaches, sandboxing, is an idea embraced by early implementations of Java (JDK 1.0.2). The idea is simple: make untrusted code run inside a box and limit its ability to do risky things. In the second approach, code signing, binary objects such as Java class files can be digitally signed by someone who “vouches” for the code. If you know and trust that person or organization, you may choose to trust the code they vouch for.JDK 1.1 introduced the notion of signed applets to Java. With the addition of signed applets, Java’s sandbox model underwent a state transition from a required model applied equally to all Java applets to a malleable system that could be expanded and personalized on an applet-by-applet basis. In fact, the distinction between applets and applications no longer applies in Java. The new way of thinking about mobile code is in terms of trust. Untrusted code needs to be restricted. Completely trusted code does not. The binary trust model designed into JDK 1.1 is not sufficiently powerful for many needs. In JDK 1.1, applet code signed by a trusted party can be treated as trusted local code, but not as partially trusted code. There is no notion of access control beyond the one and only trust decision made per class. That means JDK 1.1 offers a black-and-white trust model. When combined with access control, code-signing allows Java applets to step outside the security sandbox gradually. In fact, the entire meaning of the sandbox becomes a bit vague. JDK 1.2 implements a configurable sandbox model that can be adjusted to reflect fine-grained security policies based on signed code. As an example of how Java code-signing might work, an applet designed for use in an intranet setting could be allowed to read and write to a particular company database as long as it was signed by the system administrator. Such a relaxation of the security model is important for developers who are champing at the bit for their applets to do more. Writing code that works within the tight restrictions of the sandbox is a pain, and the original sandbox is very restrictive. JDK 1.2 code running on the new Java VMs can be granted permissions and have its access checked against policy when it runs. The cornerstone of the system is policy. Policy can be set by the user (usually a bad idea) or by the system administrator and is represented in the class java.security.Policy. Herein rests the Achilles’ Heel of JDK 1.2 security. Setting up a coherent policy at a fine-grained level takes experience and security expertise. Today’s harried system administrators are not likely to enjoy this added responsibility. On the other hand, if policy management is left up to users, mistakes are bound to be made. Users have a tendency to prefer “cool” to “secure.”
Access control and stack inspectionThe idea of access control is not a new one in computer security. For decades, researchers have built on the fundamental concept of grouping and permissions. The idea is to define a logical system in which entities known as principals (often corresponding one-to-one with code owned by users or groups of users) are authorized to access a number of particular protected objects (often system resources such as files). To make this less esoteric, consider that the familiar JDK 1.0.2 Java sandbox is a primitive kind of access control. In the default case, applets (which serve as principals in our example) are allowed to access all objects inside the sandbox but none outside it.Sometimes a Java application (say, a Web browser) needs to run untrusted code within itself. In this case, Java system libraries need some way of distinguishing between calls originating in untrusted code and calls originating from the trusted application itself. Clearly, the calls originating in untrusted code need to be restricted to prevent hostile activities. By contrast, calls originating in the application itself should be allowed to proceed, as long as they follow any security rules that the operating system mandates. The question is, how can we implement a system that does this? Java implements such a system by allowing access-control checking code to examine the runtime stack for frames executing untrusted code and compare what is happening against policy. Each thread of execution has its own runtime stack. Security decisions can be made with reference to a stack inspection [Wallach et al., 1997]. All the major vendors have adopted stack inspection to meet the demand for more-flexible security policies than those originally allowed under the old sandbox model. Stack inspection is used by Netscape Navigator 4.0, Microsoft Internet Explorer 4.0 and Sun Microsystems’ JDK 1.2 beta. For much more on stack inspection, see [McGraw and Felten, 1998].
Privileged codeFrom the introduction of JDK 1.2 beta up through JDK 1.2beta3, Sun’s JDK used the primitives
The idea is to encapsulate potentially dangerous operations that require extra privilege into the smallest possible self-contained code blocks. The Java libraries make extensive use of these calls internally, but partially trusted application code written using the JDK 1.2 model will be required to make use of them too. Correct use of the JDK primitives required using a standard try/finally block is as follows: This usage was required to address the problem of asynchronous exceptions (though there was still some possibility of an asynchronous exception being thrown in the finally clause sometime before the
Wallach and Felten first explained a particularly efficient way to implement stack inspection algorithms using multiple primitives in [Wallach and Felten, 1998]. Unfortunately, Sun decided to abandon the multi-primitive approach to stack inspection (which could benefit from the Princeton researcher’s implementation). In fact, JDK 1.2beta4 introduced a completely new API for privileged blocks. The new API removes the need for a developer to: 1) make sure to use try/finally properly and 2) remember to call
In order to properly implement the early API, VMs would have been forced to keep track of the
Bookkeeping would slow down the VM, which is about the last thing Java VMs need now as they near native C speeds. Sun has a document explaining the change (from which some of the material here was drawn) calledNew API for Privileged Blocks on the Web. The new API interface wraps the complete enable-disable cycle in a single interface accessed through a new AccessController method called
Here’s what the new usage looks like. Note the use of Java’s inner classes capability: Ironically, in Securing Java, McGraw and Felten recommend that Java developers avoid the use of inner classes to make their code more secure (see the sidebar in this article and Chapter 7 of Securing Java: Getting down to business with mobile code.) But if you want to include privileged blocks in your JDK 1.2 code, you are strongly encouraged by Sun to use them. In addition to the inner-class problem, verbosity is also a problem with the new API. It turns out that using the new API is not always straightforward. That’s because anonymous inner classes require any local variables that are accessed to be final. A small diversion can help explain why this is.
ClosuresThe new API is doing its best to simulate what programming language researchers call closures. The problem is, Java doesn’t have closures. So what are they anyway? And why are they useful?Functions in most programming language use variables. For example, the function f(x)=x+y adds the value of the formal parameter x to the value of variable y. The function f has one free variable, y. That means f may be evaluated (run) in different environments where the variable y takes on different values. In one environment, E1, y could be bound to 2. In another environment, E2, y could be bound to 40. If we evaluate f(2) in E1, we get the answer 4. If we evaluate f(2) in E2, we get the answer 42. Sometimes we want a function to retain certain bindings that its free variables had when it was created. That way we can always get the same answer from the expression. In terms of our example, we need to make sure y always takes on a certain value. What we want is a closed package that can be used independently of the environment in which it is eventually used. That is what a closure is. In order to be self-contained, a closure must contain a function body, a list of variables, and the bindings of its variables. A closure for our second example from above might look like this:
Closure is particularly useful in languages with first-class functions (like Scheme and ML). In these and other related languages, functions can be passed to and returned from other functions, as well as stored in data structures. Closure makes it possible to evaluate a function in a location and external environment that may differ from where it was created. For more on this issue, see [Friedman et al., 1992]. As we said before, Java does not have closures. Java’s anonymous inner classes come as close to being closures as Java gets. A real closure might include bindings for the variables that can be evaluated sometime in the future. But in an anonymous inner class, all state must be made final (frozen) before it is passed in. That is, the final state is the only visible state inside the inner class. This is one reason anonymous inner classes are not true closures. The problem of making everything final turns out to have strong implications for the use of the new privileged block API.
Local variablesFor example, in the following code, the variable
Making all local variables that are to be accessed in the block final is a pain, especially if an existing variable can’t be made final. In the latter case, the trick is to create a new final variable and set it to the non-final variable just before the call to
What comes out of
Another problematic issue with the new interface is the fact that the inner class always returns an object. That means if a call to a piece of privileged code, for example a call to
doPrivileged()
Whence the changeIt is good that VM vendors want their machines to be fast and efficient. But purely in terms of security, it is unclear whether the decision to change the API was a good one. Not that the previous API was perfect, but the new one seems to introduce several places where errors are bound to be made by developers charged with actually using VMs. The real answer to the problem is introducing closures to Java. Closures are something to look for in future JDK versions.
References
Related linksGary McGraw, Ph.D., is vice President of Reliable Software Technologies. Dr. McGraw is a noted authority on Java security and co-authored Java Security: Hostile Applets, Holes & Antidotes (Wiley, 1996) with Prof. Ed Felten of Princeton. They are currently writing a second book, Securing Java: Getting Down to Business with Mobile Code, available in late 1998. Along with RST’s Dr. Jeff Voas, McGraw has written Software Fault Injection: Inoculating Programs Against Errors (Wiley, 1998). Dr. McGraw also has over 50 peer-reviewed publications, consults with major e-commerce vendors such as Visa, and is principal investigator on grants from the U.S. Air Force Research Labs, DARPA and NIST’s Advanced Technology Program. John Viega is a research associate at Reliable Software Technologies. He holds a M.S. in computer science from the University of Virginia. Viega developed and maintains mailman, the gnu mailing list manager. His research interests include software assurance, programming languages and object-oriented systems. Portions of this article are taken by permission from Securing Java: Getting Down to Business with Mobile Code (John Wiley & Sons, 1998), the second edition of McGraw and Felten’s book Java Security: Hostile Applets, Holes, & Antidotes. |
Some Java books say that inner classes can only be accessed by the classes that enclose them. But that is not true. Inner classes are implemented by converting them to plain old top-level classes. That’s because most Java VM implementations have no notion of inner classes. In the end, inner classes boil down to a shorthand for top-level classes. What might seem to be a minor implementation detail actually has severe ramifications. Inner classes happen to be accessible to any code in the same package, and not just the class in which they are nested. And package-level visibility can not be relied on for security. Java packages are not closed, allowing an attacker to introduce a new class inside your package. With this new class, the attacker might access things you thought were hidden inside your inner class. It gets worse. An inner class is allowed to access the fields of enclosing classes, even if those fields are declared private. Since the inner class is treated as a separate top-level class, the way that such access is generally implemented is to silently change private fields to package accessibility. It’s bad enough that the inner class is exposed; but it’s even worse that the compiler is silently overruling your decision to make some fields private. In addition, inner classes promote a programming style that detracts from the notions of reuse and encapsulation, which are primary goals of the object-oriented paradigm. Inner classes are often tightly coupled to the class in which they are nested. The language promotes this sort of coupling by giving inner classes indiscriminate access to their containing class. So instead of having two classes talking by higher level abstractions, programmers are encouraged to let their classes communicate through private variables and methods. Such interdependencies can lead to maintenance problems. Inner classes also have a conceptually limited visibility (since they’re only supposed to be visible to the class or method in which they’re nested), which does not encourage factoring out common code. We have seen code in the real world with tons of small but similar inner classes, where all the inner classes were essentially duplicating the same behavior. For example, assume a programmer wants to use
|