Best Practices for Java Security


This post was written by Bitbucket user Anton Lawrence.


Security is an essential aspect of software development. When overlooked, development teams increase the chances of vulnerabilities and potential exploits, thus affecting the robustness of the entire application the team is working on.

It is, therefore, imperative for developers to write security-critical code throughout the application development cycle. Even if you use a solid development platform and write clean, strong code, your application may still be gullible to attacks. This happens when you're not aware of the security issues inherent in Java programming, or you do not follow secure development practices.  

The following guidelines will help you build more secure Java programs regardless of the application under development.

Input validation

Validating external input is one of the most important rules for Java Security. A basic approach would be sanitizing inputs from all untrusted sources before use. This helps you avoid potential attacks such as cross-site scripting (XSS) and SQL injection that result from maliciously crafted input that comes through external streams or method arguments.

The OWASP Java encoding library provides encoders that can help with input validation.

    <dependency>                                                                                            <groupId>org.owasp.encoder</groupId>   <artifactId>encoder</artifactId>   <version>1.2.2</version>
</dependency>
  

Another input validation approach is validating values returned through an upcall (Invoking a higher level code method). Let's consider a case where ClassLoaders return Class objects, an attacker can potentially control ClassLoader instances passed as arguments. As a result, only fewer assumptions are made when calling methods on ClassLoaders. Invoking the ClassLoader.loadClass ()multiple times does not guarantee a return of the same class definition or instance. This causes TOCTOU (time of check to time of use) issues.

When using a native method, always protect your code against buffer overflow attacks using wrappers. Unlike Java code, where runtime checks for library usage, data types, and array bounds are performed, native code is not subject to these checks. So, before invoking the native method, you should first declare it private then expose its functionality using a public Java-language wrapper method. The wrapper performs necessary input validation prior to code invocation.

    public final class NativeMethodWrapper {  
            private native void nativeOperation(byte[] data, int offset,
                                               int len);
            // wrapper method performs checks
            public void doOperation(byte[] data, int offset, int len) {
                data = data.clone();
                // validate input
     if (offset < 0 || len < 0 || offset > data.length - len) {
                      throw new IllegalArgumentException();
                }
                nativeOperation(data, offset, len);
            }
        }

  

Be careful with serialization and deserialization

Serialization creates an interface to Java classes by eluding regular field access control mechanisms such as access modifiers and constructors. As a result, remote inputs can be transformed into fully endowed objects. It is, therefore, important to avoid serialization especially when dealing with security-sensitive classes.

This is because serializing a class creates a public interface such that all class fields are accessible. Before using serialization, always consider what fields are exposed- as it could be inherently insecure for the application.

Although Oracle is on the course to do away with Java serialization, there are scenarios where serialization and deserialization are inevitable. If you have to implement them, follow these tips to make your code more secure:

  • Guard sensitive data fields
  • View deserialization as standard object construction.
  • Check all security permissions for serialization and deserialization carefully.
  • Use serialization filtering for untrusted data

A great security practice when dealing with serialization is to duplicate all security manager checks enforced in a serializable class. This way, an attacker cannot use serialization or deserialization to bypass checks in the Java Security Manager.

            public final class SensitiveClass implements java.io.Serializable {
             public SensitiveClass() {
                // Grant permission to instantiate SensitiveClass
                securityManagerCheck();
                //code follows
            }
            // implement readObject to enforce checks during deserialization
            private void readObject(java.io.ObjectInputStream in) {
                // duplicate check from constructor
                securityManagerCheck();
                // regular logic follows
            }
        }

  

Protect confidential information

Maintaining data confidentiality is a common, but often overlooked software security practice. This is probably because most Java programmers find the scope of confidentiality to be very wide. At the very least, a developer should always limit the lifetime of highly sensitive information. Personal data such as login credentials, national identity numbers, and social security numbers should never be kept longer than they are needed. Such data should not be stored in the memory or sent to log files where it can be seen by administrators.  Here is a sample non-compliant code that can expose user credentials:

  String user = a.readLine("Enter your Username:");
String pass = a.readLine("Enter your Password:");

A good idea would be storing this kind of transient data in mutable data structures and clearing them after use. For instance, you can use the Console.readPassword() method and a char array to store the password, as shown below.

  String user = a.readLine("Enter your Username:");
char[] pass = a.readPassword("Enter your password:");

After obtaining the password or any other personal information, remember to purge it from the memory after use.

Another rule of thumb when it comes to confidentiality is that you should never expose unencrypted credentials. You must encode all passwords and personal identifiable information with a one-way cipher before storing it in the database. Unencrypted personal information and credentials in the database are a security loophole.

Prevent code injection

Code injection, sometimes referred to as Remote Code Execution (RCE) is a common form of attack that involves injecting maliciously crafted code into a program, so it is executed within the application setting. In most cases, this results in an unanticipated take over as the malicious code manipulates the running application. There are various ways in which an attacker could inject code into an existing Java application, including specially created data files, modified cookies, and many more.

The best way to prevent your code from injection vulnerabilities is to ensure there is no executable user input in code. As mentioned in the first step, all user input must be sanitized before use to ensure it contains only valid and whitelisted characters.  

To avoid command injections in parameterized SQL statements that use JDBC, use java.sql.CallableStatement or java.sql.PreparedStatement instead of the dynamic java.sql.Statement.This is because higher-level libraries are more effective for insulating application code against SQL. A compliant example code could be:

  String sql = "SELECT * FROM User WHERE userId = ?";
PreparedStatement stmt = con.prepareStatement(sql); 
stmt.setString(1, userId); 
ResultSet rs = prepStmt.executeQuery();
 

You can also avoid code injection by treating all data as untrusted. And as a norm, never place untrusted data on your command line when creating a new process. It is always a good idea to pass any data to new processes as encoded arguments or in an inherited channel, or temporary files. This is because APIs like the javax.script can run scripts with untrusted executable code.

A few more helpful tips

Other than the above practices, here are a few more highly effective ways to stay abreast of the ever-changing Java security landscape:  

  • Enforce access control using the Java security manager
  • Use known, tested and secure libraries and frameworks
  • Ensure error messages do not reveal your implementation
  • If possible, consider adding an external authentication service
  • Continuous monitoring and logging of user activities
  • Always keep Java security releases up to date 

Scan your codebase for vulnerabilities using WhiteSource Bolt

A lot more goes into software security than just adhering to good development practices. In most cases, there are vulnerabilities in your code, but you might not know this until you scan the codebase. Therefore, as a best security practice, always perform a security scan using a reliable tool like WhiteSource Bolt to ensure your dependencies are safe. Bolt scans your code for vulnerabilities and suggests quick, actionable fixes for timely remediation.

 Wrap-Up

The above guidelines will help you develop more secure applications. The bottomline is that you should stay vigilant, follow these basic rules, and approach every stage of software development with a security-minded outlook.


Author bio: Anton Lawrence is a Developer turned Security Researcher. He has been living and breathing cyber security for 12 years and has developed a unique approach to his research. In his free time, Anton likes to drink a cold beer while finding HOT security vulnerabilities

Love sharing your technical expertise? Learn more about the Bitbucket writing program.

Looking to upgrade your Bitbucket Cloud plan?