Introduction
There are many ways to design secure Java objects. In this tutorial, we are going to learn how to create secure Java objects by understanding Accessibility, Extensibility, and Immutability.
Goals
At the end of this tutorial, you would have learned:
- Understand how and why to limit accessibility to a class.
- Understand how and why to limit extensibility in a class.
- Understand why Immutability is important and how to create an immutable object.
Prerequisite Knowledge
- Basic Java.
Tools Required
- A Java IDE.
Limit Accessibility
Whenever we design a Java object, we should always strive to follow the principle of least privilege, which means we should limit access as much as possible. There are 4 access modifiers in Java, from least to most accessible: private
, package-private
, protected
, and public
. Developers should make use of these access modifiers to limit accessibility to data encapsulated within a class.
Let us take a look at a bad example.
package com.example;
import java.time.ZonedDateTime;
import java.util.List;
public class InsecureBirthdaysHolder {
public List<ZonedDateTime> birthdays;
}
The code snippet above uses the public
access modifier for the birthdays List
, which anybody can access.
Maybe the developer who wrote this piece of code thought that birthdays are not sensitive data as compared to social security numbers, so they did not bother limiting access to the list of birthdays. Little did they know, birthdays can usually be combined with some other publicly available information to impersonate a person’s identity for authentication with insecure services.
A better way to write this code is:
package com.example;
import java.time.ZonedDateTime;
import java.util.List;
public class SecureBirthdaysHolder {
private List<ZonedDateTime> birthdays;
public boolean isCorrectBirthday(ZonedDateTime input){
return birthdays.contains(input);
}
}
The class above looks a lot more secure than the previous one. This time, the birthdays property is private
, therefore not visible outside of this class.
Callers can only check if a birthday is in the birthday list, but they are not able to get all birthdays anymore. This method can be coupled with a limit of 3-5 queries before locking out to prevent brute force attacks as well.
Restrict Extensibility
Java classes are open to inheritance by default, which means a malicious actor can override parent methods and change the behavior.
Consider the class below, which is used to authenticate users. This class is also open to inheritance by anybody.
package com.example;
import java.util.List;
import java.util.UUID;
public class Authenticate {
private List<UUID> uuids;
public boolean isCorrectUUID(UUID uuid){
return uuids.contains(uuid);
}
}
A malicious actor can now subclass Authenticate
and override the isCorrectUUID
method to always return true, therefore allowing all authentication attempts to succeed, bypassing checking in with the uuids list
.
class MaliciousAuthenticate extends Authenticate {
@Override
public boolean isCorrectUUID(UUID uuid){
return true;
}
}
To secure the Authenticate
class, make it final
, and any further attempt to subclass it will fail.
public final class Authenticate {
private List<UUID> uuids;
public boolean isCorrectUUID(UUID uuid){
return uuids.contains(uuid);
}
}
Immutability
Value classes should ensure that their internal values are immutable. If a reference property is mutable, attackers can modify its value even if it is marked as final. One example of a mutable API is the Date class in java.util.Date
.
Consider a value class MutableBirthday
that contains a Date
property below:
package com.example;
import java.util.Date;
public final class MutableBirthday {
private final Date birthday = new Date(2000, 1, 1);
public Date getBirthday() {
return birthday;
}
@Override
public String toString() {
return "birthday= " + birthday;
}
}
Notice that there is no setter
for the class above, but because the birthday object itself is mutable, a malicious caller can just call the getter()
to get a reference to the birthday object, and then the attacker can modify the object at will.
var mutableBirthday = new MutableBirthday();
System.out.println(mutableBirthday);
mutableBirthday.getBirthday().setDate(10);
System.out.println(mutableBirthday);
The code above prints:
birthday= Thu Feb 01 00:00:00 EST 3900
birthday= Sat Feb 10 00:00:00 EST 3900
As we can see, the caller was able to modify the internal state of the MutableBirthday
class.
A solution to the code above is to use the LocalDate
/LocalDateTime
class, which is immutable. The methods such as plusYears
or minusDays
actually return a completely new instance instead of the same instance like the Date
class does.
The code below is a much better implementation of the Birthday class.
package com.example;
import java.time.LocalDate;
public final class ImmutableBirthday {
private final LocalDate birthday = LocalDate.of(2000, 1, 1);
public LocalDate getBirthday() {
return birthday;
}
@Override
public String toString() {
return "birthday= " + birthday;
}
}
At the call site, regardless of what methods are called on the birthday reference, the original instance state does not change.
var immutableBirthday = new ImmutableBirthday();
System.out.println(immutableBirthday);
immutableBirthday.getBirthday().plusYears(10);
System.out.println(immutableBirthday);
The code above returns:
birthday= 2000-01-01
birthday= 2000-01-01
Summary
We have learned how to limit accessibility, restrict extensibility and implement immutable data classes in this tutorial.
Regarding accessibility, modules can provide another level of access control as well. Immutable data classes also have the extra benefit of being simpler to deal with when concurrency is needed.
The project code can be downloaded here: https://github.com/dmitrilc/DaniWebJavaSecureObjects