Exception handling best practices call for secure code design
Making software secure by design requires tremendous consideration about how failures are handled. Learn more from these exception handling examples.
WhatIs.com defines an exception in programming as "an unplanned event -- such as invalid input or a loss of connectivity -- that occurs while a program is executing and disrupts the flow of its instructions."
Short for exceptional event, exceptions are realities developers and engineers must plan for to ensure no security vulnerabilities are left open for hackers to exploit. However, many developers struggle to understand which circumstances are standard outcomes versus exceptions.
"Exceptions are often used to represent failures because they allow you to disrupt the normal flow of an application program," Dan Bergh Johnsson, Daniel Deogun and Daniel Sawano wrote in Secure by Design. "It's common that exceptions carry information about why and where the execution flow was disrupted. … At first glance, it seems harmless, but if you look carefully, you'll see that it reveals information you might want to keep secret."
As such, developers and engineers must understand the terminology and how to plan for it in practice.
"It's a potential way for leaking sensitive data that can lead to the attacker learning more about your system," Sawano added. "Also, it's a way to avoid having your system crash, which essentially will make it unavailable. And availability is a very important security property of systems."
In this excerpt of Chapter 9 of their book, Bergh Johnsson, Deogun and Sawano explain when to use exceptions to deal with failure, digging into how to handle exceptions -- especially when different failures use the same exception types.
Secure by Design
Click here to learn more about Secure by Design. Enter "TechTarget40" at checkout to receive a 40% discount on this title.
Download a PDF of Chapter 9.
Read a Q&A with the authors.
Visit Manning Publications to learn more about other security and tech books.
9.1 Using exceptions to deal with failure
Exceptions are often used to represent failures because they allow you to disrupt the normal flow of an application program. Because of this, it's common that exceptions carry information about why and where the execution flow was disrupted—the why is described in the message and the where by the stack trace. In listing 9.1, you see a stack trace resulting from a closed database connection. At first glance, it seems harmless, but if you look carefully, you'll see that it reveals information that you might want to keep secret. For example, the first line shows that the exception is a java.sql.SQLException. This tells you that data is stored in a relational database, and the system can be susceptible to SQL injection attacks. The same line also shows that the code is written in Java, which hints that the overall system might be vulnerable to exploits present in the language and the Java Virtual Machine (JVM).
Obviously, the level of detail in a stack trace is meant for troubleshooting rather than sharing. But why is it that stack traces get revealed to the end user every now and then? The answer lies in a combination of sloppy design and not understanding why exceptions are thrown. To illustrate this, we'll walk you through an example where sensitive business information is leaked from the domain because of intermixing business and technical exceptions of the same type. The example also helps to demonstrate why it's important to never include business data in technical exceptions, regardless of whether it's sensitive or not.
9.1.1 Throwing exceptions
As illustrated in figure 9.1, there are three main reasons why exceptions are thrown in an application: business rule violations, technical errors, and failures in the underlying framework. All exceptions share the same objective of preventing illegal actions, but the purpose of each one differs. For example, business exceptions prevent actions that are considered illegal from a domain perspective, such as withdrawing money from a bank account with insufficient funds or adding items to a paid order. Technical exceptions are exceptions that aren't concerned about domain rules. Instead, they prevent actions that are illegal from a technical point of view, such as adding items to an order without enough memory allocated.
We believe separating business exceptions and technical exceptions is a good design strategy because technical details don't belong in the domain. But not everyone agrees. Some choose to favor designs that intermix business exceptions and technical exceptions because the main objective is to prevent illegal actions, regardless of whether the illegality is technical or not. This might seem to be a minor detail, but intermixing exceptions is a door opener to a lot of complexity and potential security problems.
In listing 9.2, business and technical exceptions are intermixed using the same exception type. The main flow is fairly straightforward: a customer's accounts are fetched from a database, and the account matching the provided account number is returned. As part of this, an exception is thrown if no account is found or if an error occurs in the database.
The documentation of IllegalStateException specifies that it should be used to signal that a method has been invoked at an illegal or inappropriate time. It could be argued that not matching an account is neither illegal nor inappropriate and using an IllegalStateException is incorrect—a better choice might be IllegalArgumentException. But using IllegalStateException as a generic way of signaling failure is quite common, and we've decided to follow this pattern to better illustrate the problem of intermixing technical and business exceptions.
Throwing an exception when no account is found is logically sound, but is this a technical problem or a business rule violation? From a technical point of view, not matching an account is perfectly fine, but from a business perspective, you might want to communicate this to the user—for example, "Incorrect account number, please try again." This motivates having business rules around it, which makes the exception a business exception.
The second exception (thrown in the catch clause) is caused by a failing database connection or a malformed SQL query in the database. This also needs to be communicated, but not by the domain. Instead, you could rely on the surrounding framework to give an appropriate message—for example, "We're experiencing some technical problems at the moment, please try again later." This means the domain doesn't need rules for this exception, which makes it a technical exception. But how can you tell if you're dealing with a business or technical exception when both are of type IllegalStateException? Well, this is why you shouldn't intermix business and technical exceptions using the same type. But sometimes things are just the way they are, so let's find out how to handle this and learn what the security implications are.
9.1.2 Handling exceptions
Handling exceptions seems easy at first; you surround a statement with a try-catch block and you're done. But when different failures use the same exception type, things get a bit more complicated. In listing 9.3, you see the calling code of the fetchAccountFor method in listing 9.2. Because you want to deal with only business exceptions in the domain, you need to figure out how to distinguish between business exceptions and technical exceptions, even though both are of type IllegalStateException.
Unfortunately, you don't have much to go on, because both exceptions carry the same data. The only tangible difference is the internal message: the business exception message contains "No account matching," and the technical exception contains "Unable to retrieve account." This allows you to use the message as a discriminator and pass technical exceptions to a global exception handler that catches all exceptions, logs the payload, and rolls back the transaction due to technical problems.
But what happens if you change the message or add another business exception with a different message? Won't that cause the exception to propagate out of the domain? It certainly will, and this is how sensitive data often ends up in logs or accidentally being displayed to the end user.
In listing 9.1, you saw how stack traces reveal information that doesn't make sense to show to a normal user. Instead, displaying a default error page with an informative message would be far better; for example, the message "Oops, something has gone terribly wrong. Sorry for the inconvenience. Please try again later." A global exception handler is often used for this purpose because it prevents exceptions from propagating to the end user by catching all exceptions. Different frameworks use different solutions for this, but the idea is the same. All transactions execute via a global exception handler, and if an exception is caught, the exception payload is logged and the transaction is rolled back. This way, it's possible to prevent exceptions from propagating further, which makes it a lot harder for an attacker to retrieve internal information when a transaction fails.
Let's turn back to the accountBalance method in listing 9.3. It's obvious you can't discriminate based on the exception message, because it makes the design too fragile. Instead, you should separate business and technical exceptions by explicitly defining exceptions that are important for the business.
In listing 9.4, you can see an explicit domain exception (AccountNotFound) that signifies the event of not matching an account. The exception extends the generic type AccountException, which acts only as a marker type—a design decision that helps to prevent accidental business exceptions from leaking from the handling logic.
In listing 9.5, the fetchAccountFor method is revised to use the AccountNotFound exception instead of a generic IllegalStateException. This way, the code is clarified in the sense that you don't need to provide a message or worry about intermixing its purpose with other exceptions.
In listing 9.6, the handling logic is revised to catch the exceptions AccountNotFound and AccountException. From a security perspective, this is much better because it allows less complex mappings between business rules and exceptions, compared with using only generic exceptions such as IllegalStateException. Catching AccountException seems redundant, but this safety net is quite important. Because all business exceptions extend AccountException, it's possible to guarantee that all business exceptions are handled and that only technical exceptions propagate to the global exception handler.
Separating business exceptions and technical exceptions clearly makes the code less complex and helps prevent accidental leakage of business information. But sensitive data isn't leaked only through unhandled business exceptions. It's often the case that business data is included in technical exceptions for debugging and failure analysis as well; for example, in listing 9.5, the SQLException is mapped to an IllegalStateException that includes the account number and customer data, which are needed only during failure analysis. To some extent, this counteracts the work of separating business and technical exceptions, because sensitive data leaks regardless. To address this issue, you need a design that enforces security in depth—so let's have a look at how to deal with exception payload.
About the authors
Dan Bergh Johnsson, Daniel Deogun and Daniel Sawano are acclaimed speakers who often present at international conferences on topics of high-quality development, as well as security and design.