Encapsulation is the concept of hiding data and behavior within a class; it’s an important part of object-oriented design. It helps you write clean, modular software. In most languages, however, the visibility of data items is simply part of the relationship between the programmer and the compiler. It’s a matter of semantics, not an assertion about the actual security of the data in the context of the running program’s environment.
When Bjarne Stroustrup chose the keyword
private to designate
hidden members of classes in C++, he was probably thinking about shielding
a developer from the messy details of another developer’s code, not the
issues of shielding that developer’s classes and objects from attack by
someone else’s viruses and Trojan horses. Arbitrary casting and pointer
arithmetic in C or C++ make it trivial to violate access permissions on
classes without breaking the rules of the language. Consider the following
// C++ code
// Forge a pointer to peek inside the class
"Card Number = %.16s\n"
In this little C++ drama, we have written some code that violates
the encapsulation of the
and pulls out some secret information. This sort of shenanigan—abusing an
untyped pointer—is not possible in Java. If this example seems
unrealistic, consider how important it is to protect the foundation
(system) classes of the runtime environment from similar kinds of attacks.
If untrusted code can corrupt the components that provide access to real
resources such as the filesystem, network, or windowing system, it
certainly has a chance at stealing your credit card numbers.
If a Java application is to be able to dynamically download code from an untrusted source on the Internet and run it alongside applications that might contain confidential information, protection has to extend very deep. The Java security model wraps three layers of protection around imported classes, as shown in Figure 1-3.
At the outside, application-level security decisions are made by a security manager in conjunction with a flexible security policy. A security manager controls access to system resources such as the filesystem, network ports, and windowing environment. A security manager relies on the ability of a class loader to protect basic system classes. A class loader handles loading classes from local storage or the network. At the innermost level, all system security ultimately rests on the Java verifier, which guarantees the integrity of incoming classes.
The Java bytecode verifier is a fixed part of the Java runtime system. Class loaders and security managers (or security policies to be more precise), however, are components that may be implemented differently by different applications, such as servers or web browsers. All three of these pieces need to be functioning properly to ensure security in the Java environment.
Java’s first line of defense is the bytecode verifier. The verifier reads bytecode before it is run and makes sure it is well behaved and obeys the basic rules of the Java language. A trusted Java compiler won’t produce code that does otherwise. However, it’s possible for a mischievous person to deliberately assemble bad Java bytecode. It’s the verifier’s job to detect this.
Once code has been verified, it’s considered safe from certain inadvertent or malicious errors. For example, verified code can’t forge references or violate access permissions on objects (as in our credit card example). It can’t perform illegal casts or use objects in unintended ways. It can’t even cause certain types of internal errors, such as overflowing or underflowing the internal stack. These fundamental guarantees underlie all of Java’s security.
You might be wondering, isn’t this kind of safety implicit in lots of interpreted languages? Well, while it’s true that you shouldn’t be able to corrupt a BASIC interpreter with a bogus line of BASIC code, remember that the protection in most interpreted languages happens at a higher level. Those languages are likely to have heavyweight interpreters that do a great deal of runtime work, so they are necessarily slower and more cumbersome.
By comparison, Java bytecode is a relatively light, low-level instruction set. The ability to statically verify the Java bytecode before execution lets the Java interpreter run at full speed later with full safety, without expensive runtime checks. This was one of the fundamental innovations in Java.
The verifier is a type of mathematical “theorem prover.” It steps through the Java bytecode and applies simple, inductive rules to determine certain aspects of how the bytecode will behave. This kind of analysis is possible because compiled Java bytecode contains a lot more type information than the object code of other languages of this kind. The bytecode also has to obey a few extra rules that simplify its behavior. First, most bytecode instructions operate only on individual data types. For example, with stack operations, there are separate instructions for object references and for each of the numeric types in Java. Similarly, there is a different instruction for moving each type of value into and out of a local variable.
Second, the type of object resulting from any operation is always known in advance. No bytecode operations consume values and produce more than one possible type of value as output. As a result, it’s always possible to look at the next instruction and its operands and know the type of value that will result.
Because an operation always produces a known type, it’s possible to determine the types of all items on the stack and in local variables at any point in the future by looking at the starting state. The collection of all this type information at any given time is called the type state of the stack; this is what Java tries to analyze before it runs an application. Java doesn’t know anything about the actual values of stack and variable items at this time; it only knows what kind of items they are. However, this is enough information to enforce the security rules and to ensure that objects are not manipulated illegally.
To make it feasible to analyze the type state of the stack, Java places an additional restriction on how Java bytecode instructions are executed: all paths to the same point in the code must arrive with exactly the same type state.
Java adds a second layer of security with a class loader. A class loader is responsible for bringing the bytecode for Java classes into the interpreter. Every application that loads classes from the network must use a class loader to handle this task.
After a class has been loaded and passed through the verifier, it remains associated with its class loader. As a result, classes are effectively partitioned into separate namespaces based on their origin. When a loaded class references another class name, the location of the new class is provided by the original class loader. This means that classes retrieved from a specific source can be restricted to interact only with other classes retrieved from that same location. For example, a Java-enabled web browser can use a class loader to build a separate space for all the classes loaded from a given URL. Sophisticated security based on cryptographically signed classes can also be implemented using class loaders.
The search for classes always begins with the built-in Java system classes. These classes are loaded from the locations specified by the Java interpreter’s classpath (see Chapter 3). Classes in the classpath are loaded by the system only once and can’t be replaced. This means that it’s impossible for an application to replace fundamental system classes with its own versions that change their functionality.
A security manager is responsible for making application-level security decisions. A security manager is an object that can be installed by an application to restrict access to system resources. The security manager is consulted every time the application tries to access items such as the filesystem, network ports, external processes, and the windowing environment; the security manager can allow or deny the request.
Security managers are primarily of interest to applications that run untrusted code as part of their normal operation. For example, a Java-enabled web browser can run applets that may be retrieved from untrusted sources on the Net. Such a browser needs to install a security manager as one of its first actions. This security manager then restricts the kinds of access allowed after that point. This lets the application impose an effective level of trust before running an arbitrary piece of code. And once a security manager is installed, it can’t be replaced.
The security manager works in conjunction with an access controller that lets you implement security policies at a high level by editing a declarative security policy file. Access policies can be as simple or complex as a particular application warrants. Sometimes it’s sufficient simply to deny access to all resources or to general categories of services, such as the filesystem or network. But it’s also possible to make sophisticated decisions based on high-level information. For example, a Java-enabled web browser could use an access policy that lets users specify how much an applet is to be trusted or that allows or denies access to specific resources on a case-by-case basis. Of course, this assumes that the browser can determine which applets it ought to trust. We’ll discuss how this problem is addressed through code-signing shortly.
The integrity of a security manager is based on the protection afforded by the lower levels of the Java security model. Without the guarantees provided by the verifier and the class loader, high-level assertions about the safety of system resources are meaningless. The safety provided by the Java bytecode verifier means that the interpreter can’t be corrupted or subverted and that Java code has to use components as they are intended. This, in turn, means that a class loader can guarantee that an application is using the core Java system classes and that these classes are the only way to access basic system resources. With these restrictions in place, it’s possible to centralize control over those resources at a high level with a security manager and user-defined policy.