User Roman B.
Roman B.
Level 35
Kharkiv

Security in Java: best practices

Published in the Java Developer group
One of the most important metrics in server applications is security. This is a type of non-functional requirement.Security in Java: best practices - 1Security includes many components. Of course, it would take more than one article to fully cover all known security principles and security measures, so we'll dwell on the most important. A person well versed in this topic can set up all the relevant processes, avoid creating new security holes, and will be needed on any team. Of course, you shouldn't think that your application will be 100% secure if you follow these practices. No! But it will definitely be more secure with them. Let's go.

1. Provide security at the level of the Java language

First of all, security in Java starts right at the level of the language's capabilities. What would we do if there were no access modifiers? There would be nothing but anarchy. The programming language helps us write secure code and also makes use of many implicit security features:
  1. Strong typing. Java is a statically typed language. This makes it possible to catch type-related errors at runtime.
  2. Access modifiers. These allow us to customize access to classes, methods, and fields as needed.
  3. Automatic memory management. For this, Java developers have a garbage collector that frees us from having to configure everything manually. Yes, problems sometimes arise.
  4. Bytecode verification: Java is compiled into bytecode, which is checked by the runtime before it is executed.
Additionally, there are Oracle's security recommendations. Of course, it is not written in lofty language and you might fall asleep several times while reading it, but it's worth it. In particular, the document entitled Secure Coding Guidelines for Java SE is important. It provides advice on how to write secure code. This document conveys a huge amount of highly useful information. If you have the chance, you should definitely read it. To stoke your interest in this material, here are a few interesting tips:
  1. Avoid serializing security-sensitive classes. Serialization exposes the class interface in the serialized file, not to mention the data that is serialized.
  2. Try to avoid mutable classes for data. This provides all the benefits of immutable classes (e.g. thread safety). If you do have a mutable object, it can lead to unexpected behavior.
  3. Make copies of returned mutable objects. If a method returns a reference to an internal mutable object, then client code could change the internal state of the object.
  4. And so on…
Basically, Secure Coding Guidelines for Java SE is a collection of tips and tricks on how to write Java code correctly and securely.

2. Eliminate SQL injection vulnerabilities

This is a special kind of vulnerability. It is special because it is both one of the most famous and one of the most common vulnerabilities. If you have never been interested in computer security, then you won't know about it. What is SQL injection? This is a database attack that involves injecting additional SQL code where it is not expected. Suppose we have a method that accepts some sort of parameter to query the database. For example, a username. Vulnerable code would look something like this:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
  
   // Compose a SQL database query with our firstName
   String query = "SELECT * FROM USERS WHERE firstName = " + firstName;
  
   // Execute the query
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
In this example, an SQL query is prepared in advance on a separate line. So what's the problem, right? Maybe the problem is that it would be better to use String.format? No? Well, what then? Let's put ourselves in a tester's shoes and think about what could be passed as the value of firstName. For example:
  1. We can pass what is expected — a username. Then the database will return all users with that name.
  2. We can pass an empty string. Then all users will be returned.
  3. But we can also pass the following: "'; DROP TABLE USERS;". And here we now have huuuuuuge problems. This query will delete a table from the database. Along with all the data. ALL OF IT.
Can you imagine the problems this would cause? Beyond that, you can write whatever you want. You can change the names of all the users. You can delete their addresses. The scope for sabotage is immense. To avoid this, you need to prevent the injection of a ready-made query and instead form the query using parameters. This should be the only way to create database queries. This is how you can eliminate this vulnerability. For example:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Create a parameterized query.
   String query = "SELECT * FROM USERS WHERE firstName = ?";

   // Create a prepared statement with the parameterized query
   PreparedStatement statement = connection.prepareStatement(query);
  
   // Pass the parameter's value
   statement.setString(1, firstName);

   // Execute the query
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
This way the vulnerability is avoided. For those who want to dive deeper into this article, here's a great example. How do you know when you understand this vulnerability? If you get the joke in the comic below, then you probably have a clear grasp of what this vulnerability is all about :DSecurity in Java: best practices - 2

3. Scan dependencies and keep them updated

What does that mean? If you don't know what a dependency is, I will explain. A dependency is a JAR archive with code that is connected to a project using automatic build systems (Maven, Gradle, Ant) in order to reuse someone else's solution. For example, Project Lombok, which generates getters, setters, etc. for us in the runtime. Large applications can have lots and lots of dependencies. Some are transitive (that is, each dependency may have its own dependencies, and so on). As a result, attackers are increasingly paying attention to open-source dependencies, since they are regularly used and many clients can have problems because of them. It is important to make sure that there are no known vulnerabilities in the entire dependency tree (yes, it looks like a tree). There are several ways to do this.

Use Snyk for dependency monitoring

Snyk checks all project dependencies and flags known vulnerabilities. You can register on Snyk and import your projects via GitHub.Security in Java: best practices - 3Also, as you can see from the picture above, if a vulnerability is fixed in a newer version, then Snyk will offer the fix and create a pull request. You can use it for free for open-source projects. Projects are scanned at regular intervals, e.g. once a week, once a month. I registered and added all my public repositories to the Snyk scan (there is nothing dangerous about this, since they are already public to everyone). Snyk then showed the scan result:Security in Java: best practices - 4And after a while, Snyk-bot prepared several pull requests in projects where dependencies need to be updated:Security in Java: best practices - 5And also:Security in Java: best practices - 6This is a great tool for finding vulnerabilities and monitoring updates for new versions.

Use GitHub Security Lab

Anybody working on GitHub can take advantage of its built-in tools. You can read more about this approach in their blog post entitled Announcing GitHub Security Lab. This tool, of course, is simpler than Snyk, but you definitely shouldn't neglect it. What's more, the number of known vulnerabilities will only grow, so both Snyk and GitHub Security Lab will continue to expand and improve.

Enable Sonatype DepShield

If you use GitHub to store your repositories, you can add Sonatype DepShield, one of the applications in the MarketPlace, to your projects. It can also be used to scan projects for dependencies. Moreover, if it finds something, a GitHub Issue will be generated with an appropriate description as shown below:Security in Java: best practices - 7

4. Handle confidential data with care

We might alternatively use the phrase "sensitive data". Leaking a customer's personal information, credit card numbers, and other sensitive information can cause irreparable harm. First of all, take a close look at the design of your application and determine if you really need this or that data. Perhaps you don't actually need some of the data you have — data that was added for a future that has not come and is unlikely to come. Additionally, you many inadvertently leak such data through logging. An easy way to prevent sensitive data from entering your logs is to scrub the toString() methods of domain entities (such as User, Student, Teacher, etc.). This will prevent you from accidentally outputting confidential fields. If you use Lombok to generate the toString() method, you can use the @ToString.Exclude annotation to prevent a field from being used in the output of the toString() method. Also, be very careful when sending data to the outside world. Suppose we have an HTTP endpoint that shows the names of all users. There is no need to show a user's unique internal ID. Why? Because an attacker could use it to obtain other, more sensitive information about the user. For example, if you use Jackson to serialize/deserialize a POJO to/from JSON, then you can use the @JsonIgnore and @JsonIgnoreProperties annotations to prevent serialization/deserialization of specific fields. In general, you need to use different POJO classes in different places. What does that mean?
  1. When working with a database, use one type of POJO (an entity).
  2. When working with business logic, convert an entity into a model.
  3. When working with the outside world and sending HTTP requests, use different entities (DTOs).
This way you can clearly define which fields will be visible from the outside and which will not.

Use strong encryption and hashing algorithms

Customers' confidential data must be stored securely. To do this, we need to use encryption. Depending on the task, you need to decide which type of encryption to use. Additionally, stronger encryption takes more time, so again you need to consider how much the need for it justifies the time spent on it. Of course, you can write an encryption algorithm yourself. But this is unnecessary. You can use existing solutions in this area. For example, Google Tink:

<!-- https://mvnrepository.com/artifact/com.google.crypto.tink/tink -->
<dependency>
   <groupid>com.google.crypto.tink</groupid>
   <artifactid>tink</artifactid>
   <version>1.3.0</version>
</dependency>
Let's see what to do, using this example involving encryption and decryption:

private static void encryptDecryptExample() {
   AeadConfig.register();
   KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);

   String plaintext = "Elvis lives!";
   String aad = "Buddy Holly";

   Aead aead = handle.getPrimitive(Aead.class);
   byte[] encrypted = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encryptedString = Base64.getEncoder().encodeToString(encrypted);
   System.out.println(encryptedString);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encrypted), aad.getBytes());
   System.out.println(new String(decrypted));
}

Encrypting passwords

For this task, it is safest to use asymmetric encryption. Why? Because the application doesn't really need to decrypt passwords. This is the standard approach. In reality, when a user enters a password, the system encrypts it and compares it with what exists in the password store. The same encryption process is performed, so we can expect that they will match, if the correct password is entered, of course :) BCrypt and SCrypt are suitable here. Both are one-way functions (cryptographic hashes) with computationally complex algorithms that take a long time. This is exactly what we need, since the direct computations will take forever (well, a long, long time). Spring Security supports a whole range of algorithms. We can use SCryptPasswordEncoder and BCryptPasswordEncoder. What is currently considered a strong encryption algorithm may be deemed weak next year. As a result, we conclude that we should regularly check the algorithms we use and, as needed, update the libraries that contain the encryption algorithms.

Instead of a conclusion

Today we talked about security and, naturally, a lot of things were left behind the scenes. I just cracked open the door to a new world for you, a world that has a life of its own. Security is just like politics: if you don't busy yourself with politics, politics will busy itself with you. I traditionally suggest that you follow me on GitHub account. There I post my creations involving various technologies that I am studying and applying at work.

Useful links

  1. Guru99: SQL Injection Tutorial
  2. Oracle: Java Security Resource Center
  3. Oracle: Secure Coding Guidelines for Java SE
  4. Baeldung: The Basics of Java Security
  5. Medium: 10 tips to power-up your Java security
  6. Snyk: 10 Java security best practices
  7. GitHub: Announcing GitHub Security Lab: securing the world's code, together
Security in Java: best practices - 8
Comments (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
22 October 2020
u knoe