Rule 1: Don't Depend on Initialization
Most Java developers think that there is no way to allocate an object
without running a constructor. This is not true: There are several ways
to allocate uninitialized objects.
The easy way to protect yourself against this problem is to write your
classes so that before any object does anything, it verifies that it has
been initialized. You can do this as follows:
- Make all variables private. If you want to allow outside code to
access variables in an object, this should be done via get/set methods.
(This keeps outside code from accessing uninitialized variables.) If
you're following Rule 3, you'll make the get and set methods final.
- Add a new private boolean variable, called initialized, to each object.
- Have each constructor set the initialized variable as its last action before returning.
- Have each nonconstructor method verify that initialized is
true, before doing anything. (Note that you may have to make exceptions
to this rule for methods that are called by your constructors. If you do
this, it is best to make the constructors call only private methods.)
- If your class has a static initializer, you will need to do
the same thing at the class level. Specifically, for any class that has a
static initializer, follow these steps:
- Make all static variables private. If you want to allow
outside code to access static variables in the class, this should be
done via static get/set methods. This keeps outside code from accessing
uninitialized static variables. If you're following Rule 3, you'll make
the get and set methods final.
- Add a new private static boolean variable, called classInitialized to the class.
- Have the static constructor set the classInitialized variable as its last action before returning.
- Have each static method, and each constructor, verify that
classInitialized is true, before doing anything. (Note: Constructors are
required to call a constructor of the superclass or another constructor
of the same class as their first action. Therefore, you will have to do
that before you check classInitialized.)
Rule 2: Limit Access to Your Classes, Methods, and Variables
Every class, method, and variable that is not private provides a
potential entry point for an attacker. By default, everything should be
private. Make something non-private only if there is a good reason, and
document that reason.
Rule 3: Make Everything Final, Unless There's a Good Reason Not To
If a class or method is non-final, an attacker could try to extend it in
a dangerous and unforeseen way. By default, everything should be final.
Make something non-final only if there is a good reason, and document
that reason.
You might think that you can prevent an attacker from extending your
class or its methods by declaring the class non-public. However, if a
class is not public, it must be accessible from within the same package,
and as we shall see, Rule 4 says not to rely on package-scope access
restrictions for security.
This advice may seem harsh. After all, the rule is asking you to give up
extensibility, which is one of the main benefits of using an
object-oriented language like Java. When you're trying to provide
security, however, extensibility is your enemy; it just provides an
attacker with more ways to cause trouble.
Rule 4: Don't Depend on Package Scope
Classes, methods, and variables that are not explicitly labeled as
public, private, or protected are accessible within the same package.
Don't rely on this for security. Java classes are not closed, so an
attacker could introduce a new class inside your package, and use this
new class to access the things you thought you were hiding. (A few
packages, such as java.lang, are closed by default, and a few JVMs let
you close your own packages. However, you're better off assuming that
packages are not closed.)
Package scope makes a lot of sense from a software-engineering
standpoint, since it prevents innocent, accidental access to things that
you want to hide. But don't depend on it for security. Maybe we'll get
sealed classes in the future.
Rule 5: Don't Use Inner Classes
Some Java language books say that inner classes can only be accessed by
the outer classes that enclose them. This is not true. Java byte code
has no concept of inner classes, so inner classes are translated by the
compiler into ordinary classes that happen to be accessible to any code
in the same package. And Rule 4 says not to depend on package scope for
protection.
But wait, it gets worse. An inner class gets access to the fields of the
enclosing outer class, even if these fields are declared private. And
the inner class is translated into a separate class. In order to allow
this separate class access to the fields of the outer class, the
compiler silently changes these fields from private to package scope!
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. Don't use inner classes if you can help it. (Ironically,
the new Java 2 doPrivileged() API usage guidelines suggest that you use
an inner class to write privileged code. That's one reason we don't
like the doPrivileged() API.)
Rule 6: Avoid Signing Your Code
Code that is not signed will run without any special privileges. And if
your code has no special privileges, then it is much less likely to do
damage.
Of course, some of your code might have to acquire and use privileges to
perform some dangerous operation. Work hard to minimize the amount of
privileged code, and audit the privileged code more carefully than the
rest.
Rule 7: If You Must Sign Your Code, Put It All in One Archive File
The goal of this rule is to prevent an attacker from carrying out a
mix-and-match attack in which the attacker constructs a new applet or
library that links some of your signed classes together with malicious
classes, or links together signed classes that you never meant to be
used together. By signing a group of classes together, you make this
attack more difficult.
Existing code-signing systems do an inadequate job of preventing
mix-and-match attacks, so this rule cannot prevent such attacks
completely. But using a single archive can't hurt.
Some code-signing systems let you examine other classes to see who
signed them. If you are using a code-signing system that allows this,
you can put code into the static constructors of your classes to verify
that the "surrounding" classes have been signed by the same person as
expected. Examining signers is one way to avoid the example shown in
Figure 7.1. This doesn't completely prevent mix-and-match attacks, since
an adversary can still mix together classes that you signed at
different times; for example, by mixing version 1 of Class A with
version 2 of Class B. If you're worried about this kind of interversion
mix-and-match attack, you can put each class's "version stamp" in a
public final variable and then have each class check the version stamps
of its surrounding classes.
Figure 7.1 A mix and match attack.
In one type of mix and match attack, signed code with special privilege
is linked or otherwise grouped together with unsigned code. The danger
is that the unsigned code will be replaced in the group, leading to
undefined and possibly dangerous behavior.
|
Rule 8: Make Your Classes Uncloneable
Java's object-cloning mechanism can allow an attacker to manufacture new
instances of classes you define, without executing any of your
constructors. If your class is not cloneable, the attacker can define a
subclass of your class, and make the subclass implement
java.lang.Cloneable. This allows the attacker to make new instances of
your class. The new instances are made by copying the memory images of
existing objects; although this is sometimes an acceptable way to make a
new object, it often is not.
Rather than worry about this, you're better off making your objects
uncloneable. You can do this by defining the following method in each of
your classes:
public final void clone() throws java.lang.CloneNotSupportedException {
throw new java.lang.CloneNotSupportedException();
}
|
If you want your class to be cloneable, and you've considered the
consequences of that choice, then you can still protect yourself. If
you're defining a clone method yourself, make it final. If you're
relying on a nonfinal clone method in one of your superclasses, then
define this method:
public final void clone() throws java.lang.CloneNotSupportedException {
super.clone();
}
|
This prevents an attacker from redefining your clone method.
Rule 9: Make Your Classes Unserializeable
Serialization is dangerous because it allows adversaries to get their
hands on the internal state of your objects. An adversary can serialize
one of your objects into a byte array that can be read. This allows the
adversary to inspect the full internal state of your object, including
any fields you marked private as well as the internal state of any
objects you reference.
To prevent this, you can make your object impossible to serialize. The way to do this is to declare the writeObject method:
private final void writeObject(ObjectOutputStream out)
throws java.io.IOException {
throw new java.io.IOException("Object cannot be serialized");
}
|
This method is declared final so that a subclass defined by the adversary cannot override it.
Rule 10: Make Your Classes Undeserializeable
This rule is even more important than the preceding one. Even if your
class is not serializeable, it may still be deserializeable. An
adversary can create a sequence of bytes that happens to deserialize to
an instance of your class. This is dangerous, since you do not have
control over what state the deserialized object is in. You can think of
deserialization as another kind of public constructor for your object;
unfortunately, it is a kind of constructor that is difficult for you to
control.
You can prevent this kind of attack by making it impossible to
deserialize a byte stream into an instance of your class. You can do
this by declaring the readObject method:
private final void readObject(ObjectInputStream in)
throws java.io.IOException {
throw new java.io.IOException("Class cannot be deserialized");
}
|
As in Rule 9, this method is declared final to prevent the adversary from overriding it.
Rule 11: Don't Compare Classes by Name
Sometimes you want to compare the classes of two objects to see whether
they are the same, or you want to see whether an object has a particular
class. When you do this, you need to be aware that there can be
multiple classes with the same name in a JVM. It is a mistake to compare
classes by name since different classes can have the same name.
A better way is to compare class objects for equality directly. For
example, given two objects, a and b, if you want to see whether they are
the same class, you should use this code:
if(a.getClass() == b.getClass()){
// objects have the same class
}else{
// objects have different classes
}
|
You should also be on the lookout for cases of less-direct by-name
comparisons. Suppose, for example, you want to see whether an object
"has the class Foo." Here is the wrong way to do it:
if(obj.getClass().getName().equals("Foo")) // Wrong!
// objects class is named Foo
}else{
// object's class has some other name
}
|
Here is a better way to do it:
if(obj.getClass() == this.getClassLoader().loadClass("Foo")){
// object's class is equal to the class that this class calls "Foo"
}else{
// object's class is not equal to the class that
// this class calls "Foo"
}
|
Note the legalistic comments in the last example. Whenever you use
classnames, you are opening yourself up to mix-and-match attacks, as
described in Rule 7. You should also know that the Java language forces
you to use classnames all the time: in variable declarations, instanceof
expressions, and exception-catching blocks. Only the designers of Java
can prevent mix-and-match attacks, but you can avoid making the problem
worse by avoiding by-name class comparisons.
Rule 12: Secrets Stored in Your Code Won't Protect You
You might be tempted to store secrets such as cryptographic keys in the
code for your application or library. Secrets stored in this way are
completely accessible to anyone who runs your code. There is nothing to
stop a malicious programmer or virtual machine from looking inside your
code and learning its secrets.
Code obfuscation is another way to store a secret in your code; in the
case of obfuscation, the secret is simply the algorithm used by your
code. There's not much harm in using an obfuscator, but you shouldn't
believe that it provides strong protection. There is no real evidence
that it is possible to obfuscate Java source code or byte code so that a
dedicated adversary with good tools cannot reverse the obfuscation.