Writing secure Java code is very
difficult. There is no magic bullet that will solve your security problems; all
you can do is think hard (perhaps with help from formal analysis tools) and use
prudent engineering practices to minimize risks. Sometimes a pair of objective
outside eyes can help. The rules set forth here are intended to describe some
prudent engineering practices for writing secure Java code. They won't solve
your security problems, but they will reduce the number of ways in which things
can go wrong.
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 class
Initialized to the class.
- Have the static constructor set the class Initialized
variable as its last action before returning.
- Have each static method, and each constructor, verify
that class Initialized is true, before doing anything. (Note: Constructors
are required to call a constructor of the super class or another
constructor of the same class as their first action. Therefore, you will
have to do that before you check class Initialized.)
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.
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.
==================================================================
==================================================================
Writing secure and efficient Java code requires a combination of adhering to coding standards, understanding potential vulnerabilities, and applying prudent engineering practices. Below is a summary of the security-focused Java coding rules from your input, combined with general performance optimization guidelines:
1. Avoid Initialization Vulnerabilities
Security: Prevent uninitialized objects from being misused by attackers.
Make all fields private and use getter/setter methods.
Add an initialized boolean variable for validation in constructors and methods.
For static initialization, use a private static boolean classInitialized to ensure the static members are safe.
Performance: Lazy initialization ensures resources are allocated only when needed.
2. Minimize Access
Security: Default to private for classes, methods, and fields, exposing them only when necessary.
Document the reasons for making members public or protected.
Performance: Reducing access limits unintended interactions and improves maintainability.
3. Favor final for Extensibility and Methods
Security: Using final for classes and methods prevents malicious subclassing or overriding.
Mark classes final unless there’s a compelling reason for extensibility.
Use final for variables to improve immutability, enhancing thread safety.
Performance: final methods are optimized by the JVM as they cannot be overridden.
4. Avoid Dependence on Package Scope
Security: Classes with default (package) scope are vulnerable because attackers can introduce new classes into the same package.
Rely on private and protected access modifiers.
5. Avoid Inner Classes
Security: Inner classes can expose private fields of the outer class, as the compiler converts them into package-accessible classes.
Prefer top-level classes to reduce visibility and risks.
6. Limit Signed Code
Security: Unsigned code runs with minimal privileges, reducing potential damage.
Minimize and audit privileged code if signing is necessary.
Group signed classes in one archive to prevent mix-and-match attacks.
7. Handle Cloning and Serialization Carefully
Security: Prevent unauthorized cloning and serialization.
Use the following to block cloning:
java
Copy code
public final void clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
Block serialization with:
java
Copy code
private final void writeObject(ObjectOutputStream out) throws IOException {
throw new IOException("Object cannot be serialized");
}
Performance: Serialization and cloning can be resource-intensive; avoid unnecessary use.
8. Avoid By-Name Class Comparisons
Security: Compare class objects directly instead of by name to avoid mix-and-match attacks.
Use:
java
Copy code
if (a.getClass() == b.getClass()) { /* Safe comparison */ }
9. Avoid Hardcoding Secrets
Security: Never embed secrets like cryptographic keys directly into the code.
Use secure vault solutions like HashiCorp Vault or AWS Secrets Manager for sensitive data.
Code obfuscation provides minimal protection; focus on secure design.
10. Apply General Coding Standards
a. Optimize String Handling
Use StringBuilder for concatenation in loops.
Avoid excessive temporary string objects.
b. Efficient Data Structures
Select appropriate data structures based on usage patterns (e.g., HashMap for fast lookups, TreeSet for sorted elements).
c. Close Resources Properly
Use try-with-resources to handle files, sockets, and streams, preventing resource leaks.
d. Profile and Optimize Critical Sections
Use tools like VisualVM or JProfiler to identify bottlenecks and optimize them.
e. Avoid Reflection When Possible
Reflection is slow and can bypass security checks. Use it sparingly and with caution.
11. Secure and Validate Input
Sanitize all input to prevent injection attacks (e.g., SQL, XSS).
Use Java’s built-in validators (e.g., java.util.regex for patterns).
12. Enable Security Features
Use Spring Security or similar frameworks for authentication, authorization, and CSRF protection.
Implement secure session management by setting attributes like HttpOnly, Secure, and session timeouts.
Conclusion
By combining these security and performance principles, you can create Java applications that are robust against common vulnerabilities while maintaining high efficiency. Regularly updating knowledge about new vulnerabilities and JVM optimizations is also crucial for long-term success.
No comments:
Post a Comment