Skip to main content
  1. Posts/

CWE-778: Lack of control over error reporting in Java

Sven Ruppert
Author
Sven Ruppert
20+ years of Java, specialised in Security, Vaadin and Developer Relations. When not coding, you’ll find me in the woods with an axe.
Table of Contents

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.

Related

CWE-416: Use After Free Vulnerabilities in Java

CWE-416: Use After Free # Use After Free (UAF) is a vulnerability that occurs when a program continues to use a pointer after it has been freed. This can lead to undefined behaviour, including crashes, data corruption, and security vulnerabilities. The problem arises because the memory referenced by the pointer may be reallocated for other purposes, potentially allowing attackers to exploit the situation.

CWE-787 - The Bird-Eye View for Java Developers

The term “CWE-787: Out-of-bounds Write " likely refers to a specific security vulnerability or error in software systems. Let’s break down what it means: Out-of-bounds Write : This is a type of vulnerability where a program writes data outside the boundaries of pre-allocated fixed-length buffers. This can corrupt data, crash the program, or lead to the execution of malicious code.

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 system by preventing invalid or malicious data from causing errors or compromising the system’s integrity.

Code security through unit testing: The role of secure coding practices in the development cycle

Unit testing is an essential software development concept that improves code quality by ensuring that individual units or components of a software function correctly. Unit testing is crucial in Java, one of the most commonly used programming languages. This article will discuss what unit testing is, how it has evolved, and what tools and best practices have been established over the years.