#secureCodingPractices

If hashCode() lies and equals() is helpless

A deep look into Java’s HashMap traps – visually demonstrated with Vaadin Flow.

The silent danger in the standard library

The use of HashMap and HashSet is a common practice in everyday Java development. These data structures offer excellent performance for lookup and insert operations, as long as their fundamental assumptions are met. One of them is hashCode() of a key remains stable. But what if that’s not the case?

This is precisely where one of the most subtle and dangerous traps of the Java standard library lurks: mutable key objects. In this article, we not only demonstrate why this constellation is problematic but also illustrate the phenomenon interactively using Vaadin Flow. Readers will learn how the HashMap works internally, why equals() alone is not enough, and how to use modern language tools to generate robust, immutable keys.

  1. The silent danger in the standard library
  2. The fundamental problem: identity, hash codes and lookup
  3. The classic mistake hashCode() depends on variable attributes
  4. Security-critical consequences: When loss of consistency becomes an attack surface
  5. Interactive Demo with Vaadin Flow
  6. HashSet
  7. Strategies to avoid
  8. Why are other map implementations not affected
  9. Conclusion: The price of convenience
  10. Demo source code to try it out yourself

The fundamental problem: identity, hash codes and lookup

Internally, each HashMap is an array of buckets, where the hash code of the key determines the position of an entry. The insertion process (put(K key, V value)) looks like this: First, the map calls key.hashCode() and calculates the index in the bucket array from this value, using internal spreading. If entries already exist at this position (e.g., due to hash collisions), a linear search is performed within the bucket or – if there are enough collisions – i.e., if there are more than eight entries in a bucket and the underlying array exceeds a specific size (default value: 64) – the bucket is internally transformed from a simple linked list into a balanced binary tree structure (more precisely: a red-black tree). This conversion improves the lookup performance from linear time O(n) to logarithmic time O(log n). The decision for these thresholds is based on the observation that collisions are rare in well-chosen hash functions, and the overhead of a tree structure is only worthwhile with a high entry density. equals() is used to identify the appropriate key or create a new entry.

When accessing (get(Object key)) the same process occurs: The hash code of the transferred key is calculated again, the corresponding bucket is determined and searched for a matching key via equals() However, if the hash code of the object differs from the original put() have changed, a different bucket is addressed – the original entry then remains invisible.

Removing (remove(Object key)) is subject to the same mechanisms: the map searches for the correct bucket via hashCode() and then compares the keys using equals(). Consistency of the hash-relevant data over the entire lifetime of the key is therefore essential. Only if this is guaranteed can the HashMap ensure its efficiency and correctness.

This means: Even access to the correct bucket depends exclusively on the result of the method hashCode() This value is calculated immediately upon access, combined with an internal transformation (such as bitwise shifting and XOR operations to achieve a more even distribution across the bucket array), and then used to index the bucket array. If the return value of hashCode(). After inserting an object into the map, for example, by mutating an attribute that is included in the calculation, the newly calculated index points to a different bucket. However, the object being searched for does not exist there, which is why the map can no longer find the entry. This behaviour is not a malfunction of the HashMap, but the direct consequence of a breach of the fundamental contract hashCode() must remain consistent while maintaining the same internal object state. Violating this contract is abusing the HashMap in a way it was not designed for, with potentially serious consequences for data integrity and program logic.

The classic mistake hashCode() depends on variable attributes

Let’s take the following example: An instance of the class Person with the values name = “Alice” and id = 42 is created and stored as a key in a HashMap At the time of insertion, the map calculates the hashCode() based on the current state of the object – i.e. Objects.hash(“Alice”, 42) – and saves the entry in the corresponding bucket. After that, the field name of the object, e.g., is set to the value “Bob”. This changes the return value of hashCode(), for example, Objects.hash(“Bob”, 42), which addresses a different bucket.

If you now try to access the map again with the same object, map.get(originalPerson) – the operation fails. The map calculates a new index from the current hash code, looks in the corresponding bucket, and doesn’t find a matching entry. The original object is technically still in the map, but cannot be found.

The situation becomes even more misleading when one considers the entrySet() method. The entry is visible there, since the iteration is carried out directly via the internal chains, independent of the hash code. A comparison via equals() works with a newly created, identical object – after all, equals() is typically based on content equality and not on memory address or hash code.

This makes the HashMap de facto inconsistent: The element is still physically stored in the internal data array, but cannot be accessed via regular access paths, such as get(key), can no longer be found. To demonstrate this effect reproducibly, consider the following code example:

import java.util.*;class Person {    String name;    you hand;    Person(String name, int id) {        this.name = name;        this.id = id;    }    @Override    public boolean equals(Object o) {        return o instanceof Person p && Objects.equals(name, p.name) && id == p.id;    }    @Override    public int hashCode() {        return Objects.hash(name, id);    }}public class MutableHashDemo {    public static void main(String[] args) {        Person p = new Person("Alice", 42);        Map<Person, String> map = new HashMap<>();map.put(p, "Value");System.out.println("Before change:");        System.out.println("map.get(p): " + map.get(p));// Mutation of the key        p.name = "Bob";System.out.println("After change:");        System.out.println("map.get(p): " + map.get(p));        System.out.println("Enthält key via entrySet: " + map.entrySet().stream()            .anyMatch(e -> e.getKey().equals(p)));    }}

The demo source code is available on github at https://github.com/svenruppert/Blog—Core-Java—Mutable-HashMap-Keys-in-Java/blob/main/src/test/java/junit/com/svenruppert/MutableHashCodeDemoTest.java 

This example demonstrates that map.get(p) returns null after the change, although the entrySet() still contains the entry. The reason hashCode() returns a different value after the mutation so that the original bucket is no longer addressed. equals() alone does not help in this case, since the lookup fails due to the wrong index.

Security-critical consequences: When loss of consistency becomes an attack surface

While the loss of referentiality in a HashMap may appear at first glance to be merely a technical issue, closer inspection reveals security-relevant implications, particularly in systems that utilise state caching, authentication, or access control. If an object serves as a key in security-critical maps or sets – e.g., for detecting active sessions, auth tokens, or user permissions – and its hash code subsequently changes, a logical error condition arises. The system no longer “sees” the object, even though it still exists. This can lead to unintended access gaps or, worse still, unattended persistence of state.

A particularly dangerous scenario arises when mutable keys can be influenced from outside. One conceivable scenario is where an attacker can inject controlled values ​​into an object that then serves as a key. As soon as this key is changed—for example, through manipulated API usage or faulty deserialisation— it is removed from access control, even though it technically still exists. The result: logical access without valid authorisation.

In extreme cases, this can even lead to typical security-critical patterns, such as:

  • Authorisation Bypass: An object is modified before the test, the contains() fails, and access is granted incorrectly.
  • Resource Lock Hijack: A lock object is created via a HashSet or Map managed by the system, but it can no longer be removed after mutationa deadlock or race condition threatens.
  • Denial of service due to hash collisions: If a system stores (or mutates) many mutable objects with controlled hash codes, hash collisions can be deliberately triggered, and performance problems can be created.

Although a classic buffer overflow doesn’t occur directly in Java due to the memory safety of the JVM model, the structural effect of an inconsistent HashMap is similar to an overflow at the logical level: An access lands in the “wrong memory area” (bucket), and the object is present but functionally invisible. This is a key point for security auditors and architects: Loss of consistency in hash-based structures can not only lead to incorrect behaviour but also become a potential entry point for complex attacks.

Interactive Demo with Vaadin Flow

To make the described problem tangible, a minimalist demonstration application is recommended, e.g., with Vaadin Flow. The demo aims to allow the user to observe live how an object in a HashMap becomes “invisible” after a mutation.

The application consists of a simple Vaadin view with the following UI elements:

  • Input fields for name and ID
  • A button to insert an object into a HashMap
  • A button to modify the name (and thus the hash code)
  • A button to execute map.get()
  • A button for iterating over entrySet()

The view constructor first creates the graphical user interface. Two input fields – one for the name, one for the ID – are used to interact with the Person-Object. 

private final TextField nameField = new TextField("Name");private final TextField idField = new TextField("ID");private final TextArea output = new TextArea("Output");private final Person mutableKey = new Person("Alice", 42);private final Map<Person, String> map = new HashMap<>();

Several buttons allow you to perform specific operations on the Map. The “put(key, value)” button adds the current mutable Key-Object as key with the value “Saved” to the map. This uses exactly the object whose name and id were determined at the beginning – here “Alice” and 42.

Button putButton = new Button("put(key, value)", _ -> { map.put(mutableKey, "Saved");});

About the button “Change name”, the name of the mutableKey object at runtime. This leads to a state in which the contents of the object—and thus its hash code—change after it is inserted into the map.

Button mutateButton = new Button("Change name", _ -> { mutableKey.name = nameField.getValue();});

The “get(key)” Button demonstrates this effect: The method map.get(mutableKey) attempts to retrieve the value using the (modified) object as a key.

Button getButton = new Button("get(key)", and -> { String result = map.get(mutableKey); output.setValue("Result of get(): " + result);});

To illustrate this effect and, at the same time, provide an alternative access, the button “search entrySet()” has been added. It manually iterates through all key-value pairs of the map using the Stream API and compares the keys via equals(), regardless of the internal bucket structure. This means that an entry with the changed key object can still be found, provided equals() is correctly implemented and independent of the hashCode function.

Button iterateButton = new Button("search entrySet()", and -> { String result = map.entrySet().stream()     .filter(entry -> entry.getKey().equals(mutableKey))     .map(Map.Entry::getValue)     .findFirst()     .orElse("Not found via equals()"); output.setValue("entrySet(): " + result);});//Here is the complete source code of the view.@Route(value = PATH, layout = MainLayout.class)public class VersionOneView   extends VerticalLayout   implements HasLogger { public static final String PATH = "versionone"; private final TextField nameField = new TextField("Name"); private final TextField idField = new TextField("ID"); private final TextArea output = new TextArea("Output"); private final Person mutableKey = new Person("Alice", 42); private final Map<Person, String> map = new HashMap<>(); public VersionOneView() { logger().info("Initializing VersionOneView"); nameField.setValue(mutableKey.getName()); idField.setValue(String.valueOf(mutableKey.getId())); output.setWidth("600px"); Button putButton = new Button("put(key, value)", _ -> {   logger().info("Putting value into map with key: {}", mutableKey);   map.put(mutableKey, "Saved");   output.setValue("Inserted: "+ mutableKey); }); Button mutateButton = new Button("Change name", _ -> {   logger().info("Changing name from {} to {}", mutableKey.getName(), nameField.getValue());   mutableKey.setName(nameField.getValue());   output.setValue("Name changed to: "+ mutableKey.getName()); }); Button getButton = new Button("get(key)", and -> {   logger().info("Getting value for key: {}", mutableKey);   String result = map.get(mutableKey);   logger().info("Get result: {}", result);   output.setValue("Result of get(): " + result); }); Button iterateButton = new Button("search entrySet()", and -> {   logger().info("Searching through entrySet for key: {}", mutableKey);   String result = map.entrySet().stream()       .filter(entry -> entry.getKey().equals(mutableKey))       .map(Map.Entry::getValue)       .findFirst()       .orElse("Not found via equals()");   logger().info("EntrySet search result: {}", result);   output.setValue("entrySet(): " + result); }); add(nameField, idField, putButton, mutateButton, getButton, iterateButton, output); logger().info("VersionOneView initialization completed");} public class Person {   String name;   int id;   public Person(String name, int id) {     this.name = name;     this.id = id;   }//SNIP getter setter   @Override   public boolean equals(Object o) {     return theinstanceof Person p             && Objects.equals(name, p.name)             && id == p.id;   }   @Override   public int hashCode() {     return Objects.hash(name, id);   }   @Override   public String toString() {     return name + " (" + id + ")";   } }

This concise yet powerful view enables any reader to directly observe the effects of changing keys.

The demo source code is available on github athttps://github.com/svenruppert/Blog—Core-Java—Mutable-HashMap-Keys-in-Java/blob/main/src/main/java/com/svenruppert/flow/views/version01/VersionOneView.java  

Now, let’s modify the view slightly and display all entries from the EntrySet. The hash codes are compared, and the result is output.

Button iterateButton = new Button("search entrySet()", _ -> { logger().info("Searching through entrySet for key: {}", mutableKey); StringBuilder result = new StringBuilder(); map.forEach((key, value) -> {   boolean isMatch = key.equals(mutableKey);   result.append(String.format("""                                   Key: %s, Value: %s, HashCode %s Match: %s""",                               key,                               value,                               key.hashCode(),                               isMatch ? "And" : "No"));   result.append("\n"); }); logger().info("EntrySet search result: {}", result); output.setValue("entrySet():\n" + (!result.isEmpty() ? result.toString() : "Map is empty"));});

Now you can clearly see how an instance now exists multiple times in the same hash map. If you think about it, you can think of some very unpleasant applications that could even be used to specifically attack a system. But more on that in another blog post.

HashSet

Since a HashSet internally is nothing other than a HashMap, where the keys are the actual set elements and the values ​​are a constant dummy (such as PRESENT = new Object()), all the problems described here apply in the same way. If an object is modified after being inserted into the set, and this change affects one of the attributes that is used in the calculation of hashCode(), the object is searched in the wrong bucket. The method contains() returns false, even though the object is stored in the set. remove() fails because the same logic as get() takes effect: the HashMap finds the bucket using the current hash code and does not recognise the key there.

This behaviour is particularly critical when HashSet is used to temporarily store security-relevant information, for example, to manage already authenticated users, valid tokens, or temporary permissions. An unintentionally mutated key can lead to access being incorrectly denied or even security checks being bypassed—a classic case of a logical security vulnerability that is barely noticeable and difficult to debug.

In safety-critical modules, set elements should therefore always be immutable and defensively constructed. This means, in concrete terms, no changeable fields, no public modifiability, and – ideally – construction via a record or builder with final attributes.

Set<Person> people = new HashSet<>();people.add(p); // p.hashCode() ist Xp.name = "Malicious";System.out.println(people.contains(p)); // false

Strategies to avoid

The simplest and most effective strategy is to use only immutable objects as keys. Since Java 16, records are the idiomatically correct tool for this. They automatically generate equals() and hashCode() based on final fields.

record PersonKey(String name, int id) {}

Alternatively, if mutable state is unavoidable, the object before A modification can remove the hash code from the map and then reinsert it with an updated hash code. This is a dangerous workaround that is rarely implemented correctly in practice.

Why are other map implementations not affected

The problem described here occurs specifically in HashMap and structures based on it, such as HashSet, because a calculated hash code determines the access path. Other map implementations in the JDK – such as TreeMap, LinkedHashMap, or EnumMap – are not affected in this respect or only to a lesser extent.

The TreeMap, for example, is not based on hash code-based indexing, but on a sorted tree structure (red-black tree). It uses either the natural order of the keys (via Comparable) or a provided Comparator. This means that access does not depend on the result of hashCode() but from the stable comparability through compareTo() or compare(K1, K2). A change to an attribute that is included in the comparison logic can also lead to inconsistent behaviour, for example, get() or remove(). However, the mechanism is more transparent and controllable because sorting is done using a clearly defined comparison function.

Also, LinkedHashMap, although internally a HashMap, is not immune to the mutable hashcode problem because it uses the same bucket logic. However, it also provides a deterministic iteration order, which can aid in debugging, but does not alter the underlying problem.

The EnumMap. Finally, it is protected against this problem by design, as it is exclusively of enum types as keys. These are immutable by language definition and have stable, final hash codes. Thus, mutation is impossible, and the map remains robust against this class of errors.

You should therefore carefully consider which map implementation is used in each context, and whether a stable key state can be guaranteed or whether alternative ordering mechanisms should be used.

Conclusion: The price of convenience

In daily development, the problem is often overlooked because it only manifests itself in specific situations – for example, after multiple operations, in multi-threading, or integrations across API boundaries. However, the consequences are severe: lost entries, unexplained null values, and inconsistent state.

Anyone who wants to use HashMaps efficiently and correctly should respect their internal rules, particularly ensuring that key objects do not change subsequently.

Demo source code to try it out yourself

The complete source code for the demo view with Vaadin Flow is publicly available on GitHub. The application can be run efficiently locally using’ mvn jetty: run’. The web application will then be available at the address http://localhost:8080/.

GitHub: https://github.com/svenruppert/Blog—Core-Java—Mutable-HashMap-Keys-in-Java

Happy Coding

Sven

#Flow #Java #secureCodingPractices #Vaadin

Learn how inadequate control over error reporting leads to security vulnerabilities and how to prevent them in Java applications.

Safely handling error reports is a central aspect of software development, especially in safety-critical applications. CWE-778 describes a vulnerability caused by inadequate control over error reports. This post will analyse the risks associated with CWE-778 and show how developers can implement safe error-handling practices to avoid such vulnerabilities in Java programs.

  1. Learn how inadequate control over error reporting leads to security vulnerabilities and how to prevent them in Java applications.
  2. What is CWE-778?
    1. Examples of CWE-778 in Java
  3. Secure error handling
    1. Example with Vaadin Flow
  4. Using design patterns to reuse logging and error handling.
    1. Decorator Pattern
    2. Proxy Pattern
    3. Template Method Pattern
  5. Evaluate log messages for attack detection in real time
  6. Best practices for avoiding CWE-778
  7. Conclusion

What is CWE-778?

The Common Weakness Enumeration (CWE) defines CWE-778 as a vulnerability where bug reporting is inadequately controlled. Bug reports often contain valuable information about an application’s internal state, including system paths, configuration details, and other sensitive information that attackers can use to identify and exploit vulnerabilities. Improper handling of error reports can result in unauthorised users gaining valuable insight into the application’s system structure and logic.

Exposing such information in a security-sensitive application could have potentially serious consequences, such as the abuse of SQL injection or cross-site scripting (XSS) vulnerabilities. Therefore, it is critical that bug reports are carefully controlled and only accessible to authorised individuals.

Examples of CWE-778 in Java

The following example considers a simple Java application used to authenticate users:

public class UserLogin {    public static void main(String[] args) {        try {            authenticateUser("admin", "wrongpassword");        } catch (Exception e) {            // Error is output directly to the user            System.out.println("Error: " + e.getMessage());            e.printStackTrace();        }    }    private static void authenticateUser(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }}

This example displays an error message if the user enters an incorrect password. However, this approach has serious security gaps:

1. The error message contains specific information about the username.

2. The full stack trace is output, allowing an attacker to obtain details about the application’s implementation.

This information can help an attacker understand the application’s internal structure and make it easier for them to search specifically for additional vulnerabilities.

Secure error handling

To minimise the risks described above, secure error handling should be implemented. Instead of providing detailed information about the error, the user should only be shown a general error message:

public class UserLogin {    public static void main(String[] args) {        try {            authenticateUser("admin", "wrongpassword");        } catch (Exception e) {            // Generic error message to the user            System.out.println("Authentication failed. Please check your entries.");            // Logging the error in the log file (for admins)            logError(e);        }    }    private static void authenticateUser(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }    private static void logError(Exception e) {        // Error is securely logged without displaying it to the user        System.err.println("An error has occurred: " + e.getMessage());    }}

In this improved version, only a general error message is displayed to the user while the error is logged internally. This prevents sensitive information from being shared with unauthorised users.

Such errors should be logged in a log file accessible only to authorised persons. A logging framework such as Log4j or SLF4J provides additional mechanisms to ensure logging security and store only necessary information.

Example with Vaadin Flow

Vaadin Flow is a Java framework for building modern web applications, and CWE-778 can also be a problem if error reports are mishandled. A safe example of error handling in a Vaadin application could look like this:

import com.vaadin.flow.component.button.Button;import com.vaadin.flow.component.notification.Notification;import com.vaadin.flow.component.textfield.PasswordField;import com.vaadin.flow.component.textfield.TextField;import com.vaadin.flow.router.Route;@Route("login")public class LoginView extends VerticalLayout {    public LoginView() {        TextField usernameField = new TextField("Benutzername");        PasswordField passwordField = new PasswordField("Password");        Button loginButton = new Button("Login", event -> {            try {                authenticateUser(usernameField.getValue(), passwordField.getValue());            } catch (Exception e) {                // Generic error message to the user                Notification.show("Authentication failed. Please check your entries.");                // Logging the error in the log file (for admins)                logError(e);            }        });        add(usernameField, passwordField, loginButton);    }    private void authenticateUser(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }    private void logError(Exception e) {        // Error is securely logged without displaying it to the user        System.err.println("An error has occurred: " + e.getMessage());    }}

The `logError` method ensures that errors are logged securely without sensitive information being visible to the end user. Vaadin Flow enables the integration of such secure practices to ensure that bug reports are not leaked uncontrollably.

Using design patterns to reuse logging and error handling.

To promote the reuse of error handling and logging, design patterns that enable the modularization and unification of such tasks can be used. Two suitable patterns are the Decorator Pattern and the Template Method Pattern.

Decorator Pattern

The Decorator Pattern is a structural design pattern that allows an object’s functionality to be dynamically extended without changing the underlying class. This is particularly useful when adding additional responsibilities, such as logging, security checks, or error handling, without modifying the original class’s code.

The Decorator Pattern works by using so-called “wrappers”. Instead of modifying the class directly, the object is wrapped in another class that implements the same interface and adds additional functionality. In this way, different decorators can be combined to create a flexible and expandable structure.

A vital feature of the Decorator Pattern is its adherence to the open-closed principle, one of the fundamental principles of object-oriented design. The open-closed principle states that a software component should be open to extensions but closed to modifications. The Decorator Pattern does just that by allowing classes to gain new functionality without changing their source code.

In the context of error handling and logging, developers can write an introductory class for authentication, while separate decorators handle error logging and handling of specific errors. This leads to a clear separation of responsibilities, significantly improving code maintainability.

The following example shows the implementation of the Decorator pattern to reuse error handling and logging:

public interface Authenticator {    void authenticate(String username, String password) throws Exception;}public class BasicAuthenticator implements Authenticator {    @Override    public void authenticate(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }}public class LoggingAuthenticatorDecorator implements Authenticator {    private final Authenticator wrapped;    public LoggingAuthenticatorDecorator(Authenticator wrapped) {        this.wrapped = wrapped;    }    @Override    public void authenticate(String username, String password) throws Exception {        try {            wrapped.authenticate(username, password);        } catch (Exception e) {            logError(e);            throw e;        }    }    private void logError(Exception e) {        System.err.println("An error has occurred: " + e.getMessage());    }}

In this example, `BasicAuthenticator` is used as the primary authentication class, while the `LoggingAuthenticatorDecorator` Added additional functionality, namely error logging. This decorator wraps the original authentication class and extends its behaviour. This allows the logic to be flexibly extended by adding more decorators, such as a `SecurityCheckDecorator`, which performs additional security checks before authentication.

An advantage of this approach is combining decorators in any order to achieve tailored functionality. For example, one could first add a security decoration and then implement error logging without changing the original authentication logic. This results in a flexible and reusable structure that is particularly useful in large projects where different aspects such as logging, security checks, and error handling are required in various combinations.

The Decorator Pattern is, therefore, a powerful tool for increasing software modularity and extensibility. It avoids code duplication, promotes reusability, and enables a clean separation of core logic and additional functionalities. This makes it particularly useful in secure error handling and implementing cross-cutting concerns such as logging in safety-critical applications.

The Decorator Pattern can add functionality, such as logging or error handling, to existing methods without modifying their original code. The following example shows how the Decorator Pattern enables centralised error handling:

public interface Authenticator {    void authenticate(String username, String password) throws Exception;}public class BasicAuthenticator implements Authenticator {    @Override    public void authenticate(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }}public class LoggingAuthenticatorDecorator implements Authenticator {    private final Authenticator wrapped;    public LoggingAuthenticatorDecorator(Authenticator wrapped) {        this.wrapped = wrapped;    }    @Override    public void authenticate(String username, String password) throws Exception {        try {            wrapped.authenticate(username, password);        } catch (Exception e) {            logError(e);            throw e;        }    }    private void logError(Exception e) {        System.err.println("An error has occurred: " + e.getMessage());    }}

In this example, the `LoggingAuthenticatorDecorator` is a decorator for the class `BasicAuthenticator`. The Decorator Pattern allows error handling and logging to be centralised without changing the underlying authentication class.

Proxy Pattern

The proxy pattern is a structural design pattern used to control access to an object. It is often used to add functionality such as caching, access control, or logging. In contrast to the decorator pattern, primarily used to extend functionality, the proxy serves as a proxy that takes control of access to the actual object.

The proxy pattern ensures that all access to the original object occurs via the proxy, meaning specific actions can be carried out automatically. For example, the Proxy Pattern could ensure that authorised users can only access a particular resource while logging all accesses.

A typical example of the proxy pattern for encapsulating logging and error handling looks like this:

public interface Authenticator {    void authenticate(String username, String password) throws Exception;}public class BasicAuthenticator implements Authenticator {    @Override    public void authenticate(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }}public class ProxyAuthenticator implements Authenticator {    private final Authenticator realAuthenticator;    public ProxyAuthenticator(Authenticator realAuthenticator) {        this.realAuthenticator = realAuthenticator;    }    @Override    public void authenticate(String username, String password) throws Exception {        logAccessAttempt(username);        try {            realAuthenticator.authenticate(username, password);        } catch (Exception e) {            logError(e);            throw e;        }    }    private void logAccessAttempt(String username) {        System.out.println("Authentication attempt for user: " + username);    }    private void logError(Exception e) {        System.err.println("An error has occurred: " + e.getMessage());    }}

In this example, `BasicAuthenticator` is wrapped by a `ProxyAuthenticator`, which controls all calls to the `authenticate` method. The proxy adds additional functionality, such as access and error logging, ensuring that all access goes through the proxy before the authentication object is called.

A key difference between the Proxy Pattern and the Decorator Pattern is that the Proxy primarily controls access to the object and its use. The proxy can check access rights, add caching, or manage an object’s lifetime. The Decorator Pattern, on the other hand, is designed to extend an object’s behaviour by adding additional responsibilities without changing the access logic.

In other words, the Proxy Pattern acts as a protection or control mechanism, while the Decorator Pattern adds additional functionality to extend the behaviour. Both patterns are very useful when integrating cross-cutting concerns such as logging or security checks into the application, but they differ in their focus and application.

Template Method Pattern

The Template Method Pattern allows for defining the general flow of a process while implementing specific steps in subclasses. This ensures that error handling remains consistent:

public abstract class AbstractAuthenticator {    public final void authenticate(String username, String password) {        try {            doAuthenticate(username, password);        } catch (Exception e) {            logError(e);            throw new RuntimeException("Authentication failed. Please check your entries.");        }    }    protected abstract void doAuthenticate(String username, String password) throws Exception;    private void logError(Exception e) {        System.err.println("An error has occurred: " + e.getMessage());    }}public class ConcreteAuthenticator extends AbstractAuthenticator {    @Override    protected void doAuthenticate(String username, String password) throws Exception {        if (!"correctpassword".equals(password)) {            throw new Exception("Invalid password for user: " + username);        }    }}

The Template Method Pattern centralises error handling in the `AbstractAuthenticator` class so that all subclasses use the same consistent error handling strategy.

Evaluate log messages for attack detection in real time

Another aspect of secure error handling is using log messages to detect attacks in real-time. Analysing the log data can identify potential attacks early, and appropriate measures can be taken. The following approaches are helpful:

Centralised logging: Use a central logging platform like the ELK Stack (Elasticsearch, Logstash, Kibana) or Splunk to collect all log data in one place. This enables comprehensive analysis and monitoring of security-related incidents.

Pattern recognition: Create rules and patterns that identify potentially malicious activity, such as multiple failed login attempts in a short period. Such rules can trigger automated alerts when suspicious activity is detected.

Anomaly detection: Machine learning techniques detect anomalous activity in log data. A sudden increase in certain error messages or unusual access patterns could indicate an ongoing attack.

Real-time alerts: Configure the system so that certain security-related events trigger alerts in real-time. This allows administrators to respond immediately to potential threats.

Analyse threat intelligence: Use log messages to collect and analyse threat intelligence. For example, IP addresses that repeatedly engage in suspicious activity can be identified, and appropriate action can be taken, such as blocking the address.

Integration into SIEM systems: Use security information and event management (SIEM) systems to correlate log data from different sources and gain deeper insights into potential threats. SIEM systems often also provide tools to automate responses to specific events.

By combining these approaches, attacks can be detected early, and the necessary steps can be taken to limit the damage.

Best practices for avoiding CWE-778

To avoid CWE-778 in your applications, the following best practices should be followed:

Generic error messages: Avoid sharing detailed information about errors with end users. Error messages should be worded as generally as possible to avoid providing clues about the internal implementation.

Error logging: Use logging frameworks like Log4j or SLF4J to log errors securely. This allows bugs to be tracked internally without exposing sensitive information.

No stack traces to users: Make sure stack traces are only visible in the log and are not output to the user. Instead, generic error messages that do not contain technical details should be used.

Access control: Ensure that only authorized users have access to detailed error reports. Error logs should be well-secured and viewable only by administrators or developers.

Regular error testing and security analysis: Run regular tests to ensure that error handling works correctly. Static code analysis tools help detect vulnerabilities like CWE-778 early.

Avoiding sensitive information: To prevent sensitive information such as usernames, passwords, file paths, or server details from being included in error messages, such information should only be stored in secured log files.

Using secure libraries: Rely on proven libraries and frameworks for error handling and logging that have already undergone security checks. This reduces the likelihood of implementation errors compromising security.

Conclusion

CWE-778 poses a severe security threat if bug reports are not adequately controlled. Developers must know the importance of handling errors securely to prevent unwanted information leaks. Applying secure programming practices, such as using design patterns to reuse error-handling logic and implementing centralised logging to detect attacks in real-time, can significantly increase the security and robustness of Java applications.

Secure error handling improves an application’s robustness and user experience by providing clear and useful instructions without overwhelming the user with technical details. The combination of security and usability is essential for the success and security of modern applications.

Ultimately, control over bug reports is integral to a software project’s overall security strategy. Bug reports can either be a valuable resource for developers or, if handled poorly, become a vulnerability for attackers to exploit. Disciplined error handling, modern design patterns, and attack detection technologies are critical to ensuring that error reports are used as a tool for improvement rather than a vulnerability.

https://svenruppert.com/2024/10/18/cwe-778-lack-of-control-over-error-reporting-in-java/

#CWE778 #Java #secureCodingPractices #security

Alan E. Yue (He/Him)alaneyue@infosec.exchange
2024-09-06

FREE OWSSP Top 10 API Training Program for developers!
The OWASP API Security Risks Path is designed specifically for developers who build or work with APIs.

info.securityjourney.com/owasp

#owasp #api #top10 #securecodingpractices #free

Sven Ruppertsvenruppert
2024-08-21

CWE-377 – Insecure Temporary File in Java
In software development, temporary files are often used to store data temporarily during an application’s execution. These files may contain sensitive information or be used to hold data that must be processed or passed between different parts of a program. However, if these temporary files are not managed securely,
svenruppert.com/2024/08/21/cwe

Sven Ruppertsvenruppert
2023-12-13

Secure Coding Practices – Input Validation
What is - Input Validation?

Input validation is a process used to ensure that the data provided to a system or application meets specific criteria or constraints before it is accepted and processed. The primary goal of input validation is to improve the reliability and security of a syste
svenruppert.com/2023/12/13/sec

Client Info

Server: https://mastodon.social
Version: 2025.04
Repository: https://github.com/cyevgeniy/lmst