This month's "Under The Hood" column is the first of a four-part series about Java's security model. The four articles will focus on the security infrastructure built into the Java virtual machine (JVM) and the java.lang library. This first article gives an overview of the security model and describes the JVM's safety features.
Java's security model is one of the language's key architectural features that makes it an appropriate technology for networked environments. Security is important because networks provide a potential avenue of attack to any computer hooked to them. This concern becomes especially strong in an environment in which software is downloaded across the network and executed locally, as is done with Java applets, for example. Because the class files for an applet are automatically downloaded when a user goes to the containing Web page in a browser, it is likely that a user will encounter applets from untrusted sources. Without any security, this would be a convenient way to spread viruses. Thus, Java's security mechanisms help make Java suitable for networks because they establish a needed trust in the safety of network-mobile code.
Java's security model is focused on protecting users from hostile programs downloaded from untrusted sources across a network. To accomplish this goal, Java provides a customizable "sandbox" in which Java programs run. A Java program must play only inside its sandbox. It can do anything within the boundaries of its sandbox, but it can't take any action outside those boundaries. The sandbox for untrusted Java applets, for example, prohibits many activities, including:
- Reading or writing to the local disk
- Making a network connection to any host, except the host from which the applet came
- Creating a new process
- Loading a new dynamic library and directly calling a native method
By making it impossible for downloaded code to perform certain actions, Java's security model protects the user from the threat of hostile code.
The sandbox defined
Traditionally, you had to trust software before you ran it. You achieved security by being careful only to use software from trusted sources, and by regularly scanning for viruses just to make sure things were safe. Once some software got access to your system, it had full rein. If it was malicious, it could do a great deal of damage to your system because there were no restrictions placed on the software by the runtime environment of your computer. So, in the traditional security scheme, you tried to prevent malicious code from ever gaining access to your computer in the first place.
The sandbox security model makes it easier to work with software that comes from sources you don't fully trust. Instead of security being established by requiring you to prevent any code you don't trust from ever making its way onto your computer, the sandbox model lets you welcome code from any source. But as it's running, the sandbox restricts code from untrusted sources from taking any actions that could possibly harm your system. The advantage is you don't need to figure out what code you can and can't trust, and you don't need to scan for viruses. The sandbox itself prevents any viruses or other malicious code you may invite into your computer from doing any damage.
The sandbox is pervasive
If you have a properly skeptical mind, you'll need to be convinced that a sandbox has no leaks before you trust it to protect you. To make sure the sandbox has no leaks, Java's security model involves every aspect of its architecture. If there were areas in Java's architecture in which security was weak, a malicious programmer (a "cracker") potentially could exploit those areas to "go around" the sandbox. To understand the sandbox, therefore, you must look at several different parts of Java's architecture and understand how they work together.
The fundamental components responsible for Java's sandbox are:
- Safety features built into the Java virtual machine (and the language)
- The class loader architecture
- The class file verifier
- The security manager and the Java API
The sandbox is customizable
One of the greatest strengths of Java's security model is that two of the four components shown in the above list, the class loader and the security manager, are customizable. To customize a sandbox, you write a class that descends from
java.lang.SecurityManager. In this class, you override methods declared in the superclass that decide whether or not to allow particular actions, such as writing to the local disk. You will want to establish a custom
SecurityManager when you are using custom class loaders to load class that you don't fully trust.
As a developer, you may never need to create your own customized sandbox -- you can often make use of sandboxes created by others. When you write and run a Java applet, for instance, you make use of a sandbox created by the developers of the Web browser that hosts your applet.
The remainder of this article will discuss the Java virtual machine's safety features. Subsequent articles in this series will describe the other three prongs of Java's security architecture: class loaders, class verification, and the security manager.
Safety features built into the JVM
Several built-in security mechanisms are operating as Java virtual machine bytecodes. You have likely heard these mechanisms listed as features of the Java programming language that make Java programs robust. They are, not surprisingly, also features of the Java virtual machine. The mechanisms are:
- Type-safe reference casting
- Structured memory access (no pointer arithmetic)
- Automatic garbage collection (can't explicitly free allocated memory)
- Array bounds checking
- Checking references for
Whenever you use an object reference, the JVM watches over you. If you attempt to cast a reference to a different type, the JVM makes sure the cast is valid. If you access an array, the JVM ensures the element you are requesting actually exists within the bounds of the array. If you ever try and use a null reference, the JVM throws an exception.
Safety features and security
Because of the safety features built into the Java virtual machine, running programs can access memory only in safe, structured ways. This helps make Java programs robust, but also makes their execution more secure. Why? There are two reasons.
First, a program that corrupts memory, crashes, and possibly causes other programs to crash represents one kind of security breach. If you are running a mission-critical server process, it is critical that the process doesn't crash. This level of robustness is also important in embedded systems such as a cell phone, which people don't usually expect to have to reboot.
The second reason unrestrained memory access would be a security risk is because a wiley cracker potentially could use the memory to subvert the security system. If, for example, a cracker could learn where in memory a class loader is stored, it could assign a pointer to that memory and manipulate the class loader's data. By enforcing structured access to memory, the Java virtual machine yields programs that are robust -- but also frustrates crackers who dream of harnessing the internal memory of the Java virtual machine for their own devious plots.
Unspecified memory layout
Another safety feature built into the Java virtual machine -- one that serves as a backup to structured memory access -- is the unspecified manner in which the runtime data areas are laid out inside the Java virtual machine. The runtime data areas are the memory areas in which the JVM stores the data it needs to execute a Java application. These data areas are: Java stacks (one for each thread); a method area where bytecodes are stored; and a garbage-collected heap, where the objects created by the running program are stored. If you peer into a class file, you won't find any memory addresses. When the Java virtual machine loads a class file, it decides where in its internal memory to put the bytecodes and other data it parses from the class file. When the Java virtual machine starts a thread, it decides where to put the Java stack it creates for the thread. When it creates a new object, it decides where in memory to put the object.
Thus, a cracker cannot predict, by looking at a class file, where in memory the data representing that class -- or objects instantiated from that class -- will be kept. Furthermore, the cracker can't tell anything about memory layout by reading the Java virtual machine specification. The manner in which a JVM lays out its internal data is not part of the specification. The designers of each JVM implementation decide which data structures their implementation will use to represent the runtime data areas, and where in memory their implementation will place them. As a result, even if a cracker somehow were able to break through the Java virtual machine's memory access restrictions, he or she would next be faced with the difficult task of looking around to find something to subvert.
Safety is built in
The prohibition on unstructured memory access is not something the Java virtual machine must actively enforce on a running program; rather, it is intrinsic to the bytecode instruction set itself. Just as there is no way to express an unstructured memory access in the Java programming language, also there is no way to express it in bytecodes -- even if you write the bytecodes by hand. Thus, the prohibition on unstructured memory access is a solid barrier against the malicious manipulation of memory.
There is, however, a way to penetrate the security barriers erected by the Java virtual machine. Although the bytecode instruction set doesn't give you an unsafe, unstructured way to access memory, through native methods you can go around bytecodes.
The problem of native methods
Basically, when you call a native method, Java's security sandbox becomes dust in the wind. First of all, the guarantees of robustness don't hold for native methods. Although you can't corrupt memory from a Java method, you can from a native method. But most important, native methods don't go through the Java API (native methods provide a means of going around the Java API), so the security manager isn't checked before a native method attempts to do something that could be potentially damaging. Of course, this is often how the Java API itself gets anything done. Many Java API methods may be implemented as native methods, but the native methods used by the Java API are "trusted."
Thus, once a thread gets into a native method, the security policy established inside the Java virtual machine -- no matter what is is -- doesn't apply anymore to that thread, so long as that thread continues to execute the native method. This is why the security manager includes a method that establishes whether or not a program can load dynamic libraries, which are necessary for invoking native methods. If untrusted code is allowed to load a dynamic library, that code could maliciously invoke native methods that wreak havoc with the local system. If a piece of untrusted code is prevented by the security manager from loading a dynamic library, it won't be able to invoke an untrusted native method. Its malicious intent will be thwarted. Applets, for example, aren't allowed to load a new dynamic library and therefore can't install their own new native methods. They can, however, call methods in the Java API, methods that may be native but that are always trusted.
When a thread invokes a native method, that thread leaps outside the sandbox. The security model for native methods therefore is the same traditional approach to computer security described earlier: You have to trust a native method before you call it.
Structured error handling
One final mechanism that is built into the Java virtual machine and that contributes to security is structured error handling with exceptions. Because of its support for exceptions, the JVM has something structured to do when a security violation occurs. Instead of crashing, the JVM can throw an exception or an error, which may result in the death of the offending thread but shouldn't crash the system.
Throwing an error (as opposed to throwing an exception) almost always results in the death of the thread in which the error was thrown. This is usually a major inconvenience to a running Java program, but won't necessarily result in termination of the entire program. If the program has other threads doing useful things, those threads may be able to carry on without their recently departed colleague. Throwing an exception, on the other hand, may result in the death of the thread, but is often just used as a way to transfer control from the point in the program where the exception condition arose to the point in the program where the exception condition is handled.
Structured error handling contributes to Java's security model by helping to improve the robustness of Java programs. The Java compiler forces programmers to deal with exceptions that methods declare they may throw. This encourages programmers to write code that actually handles exception conditions that may reasonably be expected to arise as their programs run. If a program encounters a catastrophic error condition, the structure error handling mechanism enables the program to avoid an uncontrolled crash and make a more graceful exit.