by Kathryn Jones
The Java Language
The Java Compiler
The Java Interpreter
The Execution of Code
Java Virtual Machine
Limitations
Known Bugs
Future Java Security
If you have been reading the trade magazines or browsing the Java sites on the Internet, you have probably heard a lot of the buzz about security in Java. Rightfully so, as the failure to provide adequate security to protect the systems that download Java applets guarantees Java's demise. Realistically, any Internet application that downloads and runs foreign executables, and dynamically links in classes at runtime that have been created by unknown programmers, can be as dangerous as it is powerful. While these features afford you the ability to seamlessly run code from anywhere on the Web, there is no guarantee that this code is not defective. Any programmer can intentionally or unintentionally produce code that can damage your system. There is no way you can predict that the code you plan to execute is faulty. Even the code provided by reputable organizations should not be completely trusted.
You might have already read that many bugs have been discovered in Java's security system. While it is true that many bugs have been found in Java, they are primarily related to the functionality of Java and not to its security. A few important security holes have been discovered at the time of this writing, and they are either fixed in the JDK 1.01 and Netscape Navigator 2.01, or are in the process of being fixed.
These bugs should not scare you away from using Java. Java has gone through beta testing. Sun Microsystems has distributed its JDK for free to anyone who wants it, and Sun has therefore reaped the rewards of the expeditious discovery of bugs and holes in its software by a huge user base that has exposed the product to high levels of scrutiny. With each new beta release, the Java team repaired new bugs. By the time Java 1.1 is released for general availability, it should have repaired the most serious holes in its security model. Inevitably, hackers will find ways to get around Java's security, as they do with many systems, but it is in the best interest of the Java team to be committed to providing constant security updates to its product as usage by the Java community evolves.
http://www.io.org/~mentor/J___Notes.html
As you work with Java, be sure to keep an eye out for articles and announcements in magazines and on the Web about bugs and patches. One sight to add a bookmark for is Digital Espresso, a useful source of newly reported Java bugs. You can find it at the following address:
http://www.io.org/~mentor/J___Notes.html
Java's security structure is actually quite promising and is better than anything else out there today with similar power. It is a multilayered security system. This means that Java's security system is implemented in separate steps at each layer of the Java environment that code travels through before it is actually executed on your machine. Such depth of security is important, because the Java programming environment is divided into layers. It is conceivable that any Java tool in the environment can be rewritten to circumvent the Java security structure. For example, while the security specifications implemented at the language and compiler layers allow you to create code with confidence that it is reasonably secure, they do not guarantee the security of Java code run from the Internet that is created and compiled by unknown programmers. There is nothing stopping a programmer from building a new compiler that allows code with security violations to compile cleanly and therefore destroy your system if you run it. Therefore, the other layers of the Java environment must be able to subject compiled bytecode from foreign sources to extreme levels of scrutiny.
Java's runtime environment that you install on your own system trusts no executable, no class, no instruction, or no parameter. It provides several additional layers of security that interrogate code before it is executed. This chapter examines all of Java's layers of security in detail, as well as the bugs that currently exist in August, 1996.
The first place security that is implemented in Java is, appropriately, in its language. The Java language provides security through its class libraries. Chapter 11, "Reading and Writing with Java," and Chapter 12, "Network Programming with Java," cover the security features of two of the class libraries: Java.io and Java.net. To summarize, Java.net provides the interfaces to handle the various network protocols (FTP, HTTP, Telnet, and so on.). This package guards against tampering at the network interface level. The networking package can be configured at different levels of security:
Java.io provides many classes that have already been extensively tested, so it is recommended that you use these abstract classes in your code. Using Java.io classes to receive and send data to different input and output devices ensures that such activities is performed in the most secure manner. This holds true for all of Java's built-in packages. They have all been tested and should be used to ensure that your code does not violate any rules.
The Java language also adds security by providing access restrictions for encapsulation of classes, methods, and variables. Any class not declared public, for instance, is inaccessible by foreign classes. Any class declared protected is accessible only by its objects and subclasses. Any class declared private limits access to objects instantiated from it.
The Java language also eliminates pointer arithmetic and prevents you from explicitly controlling pointers in any manner. Instead, the compiler assigns symbolic references to methods, and the interpreter automatically assumes the responsibility of managing memory allocation and deallocation. As you have learned, pointers and pointer arithmetic used in other C-type programs are a leading source of bugs that crash systems.
Moreover, you can use the Java language to create your own security manager. The Java runtime environment has its own security manager that is constantly active at runtime. This security manager is an object that authorizes all operations before they are executed. The security manager throws a SecurityException if it rejects an operation. Otherwise, it passes the operation and allows it to run.
The Java.lang package provides an abstract security manager class that you can subclass to create your own security manager. The class provides methods that inspect classloaders on the execution stack.
Note that you cannot install a new security manager in an applet. Applets are subject to the security manager of the application in which they are running (your browser or Java Appletviewer).
The Java compiler not only checks that your syntax is correct in your source code that was created in the Java language, but it ensures that the code doesn't violate the language's safety rules. The compiler ensures that you have not made any errors, such as casting objects that are incompatible or using incorrect parameters.
As discussed in Chapter 2, "Getting Started," the Java Compiler works similarly to compilers in C-type languages in that it takes intelligible source code and converts it to code for a machine to interpret. The difference is that the machine that the Java compiler compiles for is the Java Virtual Machine, and the code is not native machine code for your CPU, it is bytecode for the JVM. Additionally, the Java compiler does not convert references to numbers and does not create a memory layout for the program at compile time. Although performance takes a hit since references in Java must be looked up in an object index at runtime instead of referring to exact memory addresses with the code, these changes were made for security reasons.
The compiler enforces sizes for bytecode commands and symbolic address references it creates. Each bytecode command consists of an opcode and an operand. The opcode is the command that the interpreter recognizes. The operand is the data needed by the opcode. Opcodes are executed sequentially and stored in 8-bit numbers. Operands vary in length, but are divided into bytes. Each opcode has a 32-bit symbolic address reference, or handle. The Interpreter is able to locate pieces of code in memory using the opcodes assigned by the compiler. It is important that these sizes remain constant for portability, and the compiler ensures that they are.
The Java interpreter performs many functions, some of which are performed solely for the purpose of the security of the system, and others that are performed as a part of the execution of the Java application, but require that security is enforced at each step.
One function that the interpreter performs for the purpose of security is laying out the memory map at runtime. This is unlike C and C++, in which the memory map is laid out by the compiler. The interpreter's allocation of memory in a Java application might vary depending on the user's hardware and software platform. This prevents a hacker from predicting where a class exists in memory, and then directly manipulating it.
Because memory is allocated by the runtime interpreter, Java has the luxury of eliminating the use of pointers in the language that explicitly addresses memory space. This prevents an innocent programmer from accidentally placing the wrong memory address in the code for a method, which would result in crashing your system. The compiled code references memory with handles that are resolved to exact memory addresses at run time by the Java interpreter. You are unable to forge pointers to memory in Java, because the memory layout and object index do not exist until runtime and are controlled entirely by the Java interpreter.
Without pointers to locate a method, for example, the Java interpreter's memory layout is used to locate the method during runtime. When a method is called for the first time in a program, the interpreter refers to an object index of symbolic references, created by the compiler, that it checks against the memory layout it has created and finds where it placed the method in memory when the class was loaded. Subsequent calls to this method do not require such a lookup because the index contains the proper memory address.
Such symbolic references solve the fragile superclass security problem, which occurs in programs created in C and C++ when a superclass has been updated, possibly changing the memory layout. If a subclass tries to call a method from the updated superclass, its placement might be different in memory, and the program jumps to an obscure area of memory, inadvertently jeopardizing the system. In Java, the subclass calls methods symbolically from the superclass, and the interpreter locates the method using its memory layout and object index. Therefore, the correct method is called from the correct area of memory every time.
In addition to the security inherent in its runtime memory layout and object index referencing, the interpreter enforces security in three layers: the class loader, bytecode verifier, and runtime system. The class loader brings in the Java file, plus any classes that are referenced or inherited by the classes in the code. The bytecode verifier then ensures that the code adheres to Java standards and doesn't violate the integrity of your system. The runtime system executes the code on your hardware.
The class loader is responsible for loading classes that are called while a Java program is executing and laying them out in memory in such a way that they are not able to interfere with each other without explicit measures set forth in the language. It loads both local classes and foreign classes that have been determined clean by the bytecode verifier.
You can think of the execution environment of a Java application as a set of classes that are partitioned into separate namespaces. The class loader provides a layer of security by placing incoming classes in their own namespaces. Classes do not interfere with classes in other namespaces, or partitions, without explicit calls to their symbolic references and the permission of the target class to be accessed by the foreign classes (the target class must not have declared any access restrictions).
The class loader assigns one namespace for all of the classes that come from the local file system (built-in Java classes), and a separate name space for the each source of imported classes. This protects local classes from foreign classes. When a class references another class, it first searches the local system's namespace, then in the namespace of the referencing class. Foreign classes have no way of simulating a local class. Likewise, built-in classes cannot interfere with imported namespaces without referencing their classes explicitly. Foreign classes are similarly partitioned from each other because they are each assigned their own namespaces.
The Java Interpreter passes all incoming code to a bytecode verifier. The responsibility of the bytecode verifier is to subject every piece of code that the interpreter passes it to a rigorous series of integrity tests. It performs a variety of tests that run from simple verification that the format of a line of code fragment is consistent with the language specification, to passing each line of code through a theorem prover to trap the following types of problems:
After code has been approved by the bytecode verifier, you can be reasonably sure that the language does not violate your system with harmful instructions that fit any of these conditions. To maintain system performance, after code passes the verifier tests and is approved it will not be checked again. This allows the interpreter to reliably execute the code at full speed without stopping to check its integrity.
The Java class loader and bytecode verifier make no assumptions about the primary source of the bytecode stream. The code may have come from the local system, or it may have come from a system in another country. The bytecode verifier is the last line of defense against errant code. Java requires that imported code passes the verifier's tests before it is executed by any means on the system.
Once the code has been loaded, laid out in memory, and verified, it is executed a piece at a time by the interpreter. The interpreter can execute bytecodes that have been coded for the Java Virtual Machine specification directly. It also provides a just-in-time compiler that compiles intermediate bytecode to native machine code at runtime for cases that you are willing to sacrifice portability to allow the bytecode to run at full speed. Security can be implemented at runtime by coding traps and exception handlers into your program.
At this point in the book, you might be curious as to how the Java Virtual Machine actually works. A grasp of the fine points of the JVM gives you a greater understanding of the security structure of Java. This section unravels the mystery of the JVM.
The JVM is intended to provide a set of specifications that the Java language, compiler and interpreter adhere to in order to ensure secure, portable programs and runtime environments. The JVM provides a strict set of rules that can be used by a developer to create an original implementation of an interpreter that runs Java code on any machine it is installed on. These rules require that the runtime interpreter include all of the following pieces:
When Java code is compiled, it is converted to bytecode, which is similar to the assembly language created by C and C++ compilers. Each instruction in the bytecode contains an opcode followed by an operand. The following list contains some examples of opcodes and their descriptions:
Opcodes are represented by 8-bit numbers. Operands vary in length. They are aligned to eight bits, and therefore operands larger than eight bits are divided into multiple bytes. The reason Java uses such small memory spaces is to maintain compactness of memory. The Java team felt that compact code was worth the performance hit on the CPU while locating each instruction, a hit that results from the inability of the interpreter to judge exactly where each instruction is due to the varying lengths of instructions. This decision reclaims lost performance as compact bytecode travels across networks more quickly than code found in other programming languages that contains unused memory space left free as a result of larger, fixed instruction lengths. Of course, code with fixed instruction lengths runs more quickly on the CPU as the interpreter can jump through instructions, anticipating their lengths and exact locations.
The instruction set provides specifications for opcode and operand syntax and values, and identifier values. It also includes instructions for invoking methods.
Opcode recognizes the primitive data types described in Chapter 1, " An Overview of Java." In addition, it recognizes the symbolic object reference, which is a type of 32-bit length. The Java compiler manages these types. It assigns bytecodes that are appropriate for each type and each method.
The JVM contains four 32-bit registers that store information about the current state of the system. These registers are updated after the execution of each bytecode.
The processor of your machine deals quickly with these registers.
The Java stack provides the current parameters to bytecodes during execution of methods. Each method of a class is assigned a stack frame that is stored in the Java stack. Each stack frame holds the current status of local variables, the operand stack, and the execution environment.
The local variables for the method are stored in an array of 32-bit variables indexed by the vars register. Larger variables are divided across two local variables. When local variables are used, they are loaded onto the operand stack for the method. The operand stack is a 32-bit first in, first out (FIFO) stack that stores operands for opcodes in the JVM instruction set. These operands are both parameters used in methods' instructions, as well as results of instructions. The execution environment provides information about the current state of the method in the Java stack. It stores pointers to the previous method, pointers to its local variables, and pointers to the top and bottom of the operand stack. It might also contain debugging information.
As you learned in Chapter 4, "Creating Your Own Objects," Java's garbage collector keeps track of references to objects allocated in memory using symbolic handles. When an object is no longer being referenced during the execution of the program, the garbage collector returns the memory used by the object to its garbage collection heap. This heap is a separate area of memory in Java that is allocated when the runtime system is started. It is provided specially for allocation of memory to new objects. If the system the interpreter runs on supports virtual memory, the size of the garbage collection heap can grow as necessary.
The other memory areas provided in the JVM are for storing methods and the constant pool. All of the bytecode for Java methods is stored in the method area. It also stores symbol tables for dynamic linking of classes and additional debugging information associated with a method. The constant pool area encodes string constants, class names, method names, and field names for each class. It is created by the Java compiler. These memory areas are not required to be laid out in any particular location to avoid exposure to hackers who would be able to find their code if they knew the memory map before runtime.
The JVM in JDK 1.01 has a few limitations due to its fixed operand and stack sizes that may be resolved in future releases of the JDK.
These limitations are not issues today because 4G of internal addressing space is not necessary on today's machines that typically have 16 or 32M RAM. However, technology advances quickly, so this limit could conceivably become an issue in the near future. Keep in mind that these limitations might be relaxed in later releases of Java.
It is important that you are aware of the bugs in Java that have already been discovered and reported before you spend time and energy struggling with them. The latest release of the Java Developer's Kit is version 1.0.1. You should be aware of several bugs that exist in this release. This section lists the open bugs in the JDK 1.0.1 as reported by Sun Microsystems, some workarounds and patches that Sun suggests using, the JDK 1.0 security bugs that have been announced by Sun as fixed in JDK version 1.0.1. and some newer security bugs that have not yet made it to Sun's official list of open bugs.
Additional bugs will be discovered as Java is used by an increasing number of programmers in increasing capacities. It is important that you regularly check Java's Web site for bugs that Sun confirms, and additionally, unofficial Java Web sites, like Digital Espresso, for one, reports bugs that have not yet been confirmed by Sun.
Keep in mind that the bulk of these bugs address minor problems with the functionality of Java, and not with Java's security.
The following bulleted list of bugs, organized by the section of the Java system in which they exist, are currently reported by Sun as open in version 1.0.1 of the JDK. Many of them were discovered in the JDK 1.0 and have not yet been fixed, some list workarounds that Sun suggests using. You can find an updated list of open bugs for the latest JDK similar to this list on Java's Web site.
The following bugs are related to Java's runtime system.
The following bugs are related to Java's Applet Viewer.
The following bugs are related to Java's compiler.
package pkg; import java.util.*; public class Vector{ private int i; public Vector(){ i = 1; } }
/* file CompilerTest.java */ class CompilerTest { // class definition here } /* file compilertest.java */ class compilertest { // class definition here void function(CompilerTest ct) { // do some work here } }
The following bugs are related to Java's debugger and other Java tools.
The following bugs are related to java.awt.
The following bugs are related to java.awt.image.
if (!grabbing) { producer.startProduction(this); grabbing = true; flags &= ~(ImageObserver.ABORT); }
if (!grabbing) { grabbing = true; flags &= ~(ImageObserver.ABORT); producer.startProduction(this); }
The following bugs are related to Java's input/output package.
The following bugs are related to java.net.
java.net.MalformedURLException: unknown protocol: ftp
The following bugs are related to java.util.
/* Create new date object for 28-Feb-1996 */ Date date1 = new Date(96, 01, 28); /* Do some work here */ /* Need to set the date to one week after this date * This will correctly create a new date of 06-Mar-96 */ date1 = new Date( date1.getYear(), date1.getMonth(), date1.getDate() + 7, date1.getHours(), date1.getMinutes(), date1.getSeconds() );
Tue Jun 27 13:34:37 PDT 1995
Tue Jun 27 13:34:37 1995
Sun Microsystems reports that the following security bugs have been fixed between JDK 1.0, JDK 1.0.1 and Netscape Navigator 2.01. The bulleted list that follows describes each bug and its fix as reported by Sun or Netscape:
The following bugs were recently reported to Sun, and fixes are in the works. These bugs will most likely be fixed in the release following JDK 1.0.1.
Applet exception: class Navigator got a security violation: Untested Host Translation:
Many organizations are currently racing to produce tools that will improve Java security. These organizations are creating their own bytecode verifiers, runtime environments, compilers, and virus scanning software. They are working on providing encryption of binaries that will make it difficult for hackers to reverse engineer Java code. They are developing internal versioning systems and object directory services that will allow for authentication of applets.
Two examples of such organizations are Symantec, which has recently announced its virus scan software package for Java, and The University of Illinois Systems Software Research Group, which has recently announced the release of a package that gives the Java programmer access to a security API. The details as announced follow in this press release:
"Symantec Corporation, a leading supplier of utilities software products, today announced the development of new technology that lays the foundation for delivery of leading edge antivirus solutions. The Symantec AntiVirus Research Center (SARC) has developed the first native-Java virus scanner for Java applets sent over the Internet. In addition, SARC has also designed an in-house automation technology that can be used to analyze, replicate, detect and define a large subset of the most common computer viruses.
One of the fastest growing development environments is Java. While no current Java virus threats exist, there is a possibility that a virus could be written. In addition, due to Java's inherent portability, a virus of this type could spread over a wide variety of platforms. To address this possibility, SARC has produced a Java class file scanner extension for NAV. This will enable NAV to provide real-time protection and monitor for Java virus activity within Netscape or any other Java supported web browser.
The current AutoProtect capability in Norton AntiVirus (NAV) is configured to scan Java applets sent over the Internet in .CLASS files, and can detect a potential type of Java (Java Type I) virus that can be propagated by modifying HTML pages.
The new Java scanner technology can detect another, more complex, type of Java virus (Java Type II) that parasitically infects .CLASS files. The Java .CLASS file scanner provides a much faster and more efficient scan than that achieved with conventional brute-force scanning technology and represents the best scanning technology available today for the Java environment. At the first sign of a Java virus threat, Symantec will make this technology available to customers via an immediate virus definition update."
http://choices.cs.uiuc.edu/Security/JGSS/jgss.html
This is a press release by the University of Illinois:
The University of Illinois Systems Software Research Group has released the first alpha version of their JGSS Java package. This package provides Java programs access to the Generic Security Service API defined in RFC-1508 and implemented by MIT's Kerberos system. The package is available for download for personal, educational, and research use at the following address:
http://choices.cs.uiuc.edu/Security/JGSS/jgss.html
The status of security in Java is ever-changing. This chapter has provided you with a solid comprehension of the inner workings of Java's security structure as it exists today. To remain informed of that state of this structure as it grows and changes, and to be sure that you are effectively protecting your code and your computer environment from hackers, you must be sure to regularly check Java's Web site, other Java sites, and magazine articles for announcements of new changes, additional functionality, patches, and bugs as they arise.