Table of contents:

This post is about the dangers of ignoring return values in functions and API calls (where I see exceptions as another form of returned value).

Introduction

Functions are the primary building blocks of almost all computing languages. For the most part, they are very similar to mathematical functions. A function accepts zero or more parameters, processes them and gives you a result. Different from a mathematical function, most definitions of functions in computing languages allow for doing extra:

  1. Some functions may not return anything at all, doing their business behind closed curtains. They are sometimes called procedures, void functions, or just functions that return nothing.
  2. Functions may have side effects. That means that, instead of processing only the provided parameters and returning a result that depends only on its parameters, some functions may change a global variable elsewhere in the code, print stuff on the screen or change a table in a distant database.
  3. Side effects may proceed to the extent that in some languages it is possible to alter the values of the input parameters.
  4. In some languages, you can see the idea of exceptions. These are essentially intended for use in case something unexpected occurs and the function cannot proceed further, such as division by zero or trying to access an inexistent file. Exceptions are an essential aspect of functions. Even in languages that exceptions don't exist (for example, C), return values are used as a replacement for signaling the caller of the function about the success/failure status of the instructions executed.

Therefore, we can see functions in most computing languages as chunks of code that run as if it's one piece of code and:

In terms of sane coding, all these three different outcomes of a function need to be taken into account for a resulting software that doesn't do unexpected things in times of stress. A program that makes use of a function (probably written by someone else) that ignores the outcomes might not be taking into account all of the intentions of the function, thus ending up with a program that exhibits unintended behavior.

Security implications that arise from ignoring are evident. Let's first consider a case where ignoring a returning result has dire effects:

Ignoring the Return Value of a Function

Consider this C++ function that authenticates the current user for a privileged action on behalf of another user, runs stuff and reverts the program back to its initial privilege level:

void actOnBehalf(__in HANDLE hNamedPipe, void(*doSomething(void)) {
  ImpersonateNamedPipeClient ( hNamedPipe ) ;
  doSomething ( ) ;
  RevertToSelf ( ) ;
}

Let's detail this example: Our procedure actOnBehalf runs with administrator privileges first. It first calls ImpersonateNamedPipeClient, which changes the security level of the running code to the client requesting the code to be run. For details, see 1. Then it does what is supposed to do on this level of privilege by doSomething (which is another function). When all is done, it assumes previous privileges by RevertToSelf2. Have a look at the documentation on ImpersonateNamedPipeClient and RevertToSelf functions on 1 and 2, you will see that they return BOOL, which is nothing but true or false, signaling how well they did their jobs.

Assume that ImpersonateNamedPipeClient cannot complete “impersonating whatever it's supposed to impersonate”. This means that doSomething will happily run anyway but with administrator privileges, rather than possible a normal user that requested it to be executed. This function may access anywhere the administrator can. So an attacker will do his/her best to make ImpersonateNamedPipeClient to fail. At this point, we won't ponder on methods of exploitation but there are many links out there in the wild which detail what can be done.

Similarly RevertToSelf might also fail, so if there are any subsequent steps in the code, they won't be executed with the right privileges.

Such problems can be averted in the first place of checking the outcomes of external functions. Something like this would help a lot:

void actOnBehalf(__in HANDLE hNamedPipe, void(*doSomething(void)) {
  if ( ImpersonateNamedPipeClient ( hNamedPipe ) ) {
    doSomething ( ) ;
    RevertToSelf ( ) ;
  }
}

(As an exercise, consider handling the result of RevertToSelf. Would you throw an exception, do something else, or just stop the code from running?)

Ignoring Exceptions

Exceptions are a much-debated topic in programming. There are many stances for and against exceptions. Moreover, exceptions are generally defined in two different types, checked and unchecked. Let's stick to Java language for now, and focus on what can be known about exceptions in Java.

The concept of catching exceptions is about specifying some code to the program that instructs what to do when an exception is thrown. All exceptions can be caught, but it's imperative to catch checked exceptions or the program won't compile, whereas catching unchecked exceptions is optional. Uncaught exceptions end up with an abrupt interruption of code. The rule of thumb is: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception4.

Regarding Java, we can say that ignoring exceptions comes in two flavors:

  1. Ignoring unchecked exceptions
  2. Tricking the compiler to ignore checked exceptions or giving the false impression that an exception is handled

Ignoring Unchecked Exceptions

Let's consider the first case where we ignore an unchecked exception. Our example is about the NumberFormatException5, an excerpt from a hypothetical server code that accepts an integer N from the user and replies back N times. Because the user may input anything, the function accepts any string and parses it as an integer.

void do(String input) {
  int n = Integer.valueOf(input);
  for (int i = 0; i < n; i++) {
    reply();
  }
}

(There are many other things to consider in this function such as input validation and number overflow problems, but let's focus on the unchecked exceptions here.)

Immediate result of user providing an invalid input (a string that can't be parsed to an integer, such as “asdf") will raise a NumberFormatException and the program will prematurely exit on the step where we instruct the computer to get the valueOf the input: A typical DOS (denial of service) method by an inexperienced attacker.

As we said before, we won't dive into input validation but even if we sanitize the parameters and reject invalid input, we still need to handle the case where integer parsing fails - consider the case where the input parameter is a perfectly acceptable sequence of numbers but Java can't turn it into an integer because it's too large to fit in the designated data structure.

void do(String input) {
  try {
    int n = Integer.valueOf(input);
    for (int i = 0; i < n; i++) {
      reply();
    }
  } catch (NumberFormatException e) {
    // handle the error
  }
}

The part where we handle the error requires more debate than a one-line solution suitable for all needs. Handling the error depends on where the error happens. In our simple case, it might be just fine to ignore this error and do nothing regarding the input. On a more interactive level, we may consider replying to the user with a mind-your-manners warning that the input is invalid. On a larger and more sensitive scale, we may even try to figure out if it's a repeated attempt to cause trouble and take necessary measures such as blocking the source of input.

Most automated bug-finding tools will warn if you just ignore the error as in the example. Sometimes it may feel the easiest way to add some logging and silence your favorite bug-finding tool. But consider this: Is it worth logging? When you see the related log entry, is there anything you can do to fix this? Your users will always input wrongly from time to time and sometimes it's best to do ignore them. Immediate disclaimer though: Ignoring an exception is exceptional and it must be documented.

So, what about handling exceptions properly so that nobody will frown on code review? Let's study this part with an example of a checked exception:

void do(String input) {
  try {
    int n = Integer.valueOf(input);
    for (int i = 0; i < n; i++) {
      reply();
    }
  } catch (NumberFormatException e) {
    // handle the error
  }
}

One unchecked exception that you don't want to mess up with is NullPointerException. This is an exception that deserves avoiding, rather than handling later on. Consider this case, where we implement an “echo back” function, only if the input is long enough for our purpose:

String echo(String input) {
  if (input.length() > 10) {
    return input;
  }
  return "not long enough";
}

If the input is null, we will face a NullPointerException. It's perfectly valid to catch it:

String echo(String input) {
  try {
    if (input.length() > 10) {
      return input;
    }
    return "not long enough";
  } catch (NullPointerException e) {
    return "nothing really";
  }
}

However, ending up with a NullPointerException is almost always something that can be avoided by careful coding and following best practices. We won't detail all possible solutions here, but suffice it to say that checking for possible null values before operating on them is the general cure to this specific exception.

One question might be about how to know all the unchecked exceptions a function can throw. It of course always makes sense to inspect the source code, but there will always be extra exceptions that propagate from a deeper function than the function you are actually calling. In these cases, documentations (hopefully in Javadoc format) should help, and testing for edge cases that may reveal run time errors will increase your confidence about the coverage rate of your code handling exceptions.

Ignoring Checked Exceptions

As we said earlier, it's not possible in Java where you don't catch a checked exception. However, doing nothing about is more or less the same as ignoring it:

...
try {
  ...
  doSomething;
  ...
} catch (Exception e) {
  // come on, what's the worst that could happen?
}
...

This example ignores an exception but it doesn't stop there: It also ignores the mother of all exceptions - Exception, which is considered a grave error even in more liberal circles of coding. This example instructs the computer to ignore an exception, whether it's as avoidable as a NullPointerException or as investigatable as SQLException, and continue computing as if nothing happened. This not only leads to unpredicted behavior but also leaves us in the dark as to what happened when the program starts acting funny.

There are many ways to handle an exception and optionally recover from it. The actual solution needs to take into account the current state of the code. But regardless of the context, here are some principles to take into account:

  1. Catch all proper exceptions, and catch them separately first, joining them if it really makes sense. For example, instead of using one catch (Exception e) or one catch (NumberFormatException | IOException e), first try catching them separately, and only join exception handling blocks if you end up doing the same thing handling them:
try {
  ...
} catch (NumberFormatException e) {
  ...
} catch (IOException e) {
  ...
}
  1. In your tests (you are writing unit tests with your code, right?) make sure to exercise exceptions to ensure they're handled properly.

  2. Never ignore exceptions: Don't ignore run time exceptions and don't put no-operation code in catch blocks.

  3. Rethrow an exception if it's not possible to recover it on the current level of code.

  4. If you are logging exceptions, don't include sensitive information. Something like “user john tried to login with password 123456 but we couldn't because of SQLException …” is unacceptable because it clearly states that the password might be correct (and accidentally decrypted to a file) and it's just that we can't check the database to verify it.

  5. Logging stack traces and configuration data is about as sensitive as logging plain-text passwords. Such logs, when communicated with a user or some other component of the system may leak configuration secrets or insights about the code.

The definitive guide to exceptions in Java is in 3. Note that unchecked exceptions are also called runtime exceptions in Java.

Ignoring Side Effects of a Function

Apart from return values and exceptions, a function may alter data that can be accessed from other functions, alter states of peripheral devices such as screens or printers and can interact with external systems such as a remote web site or a database. Every developer's dream is to have functions with no side effects at all, but this would surely be a function that does nothing to make the world a better place.

Ignoring them doesn't mean “not handling” them, as was the case in exceptions and return values, because side effects are meant to be handled elsewhere, by definition. We must make sure that some code that is meant to print prints correctly and a function that needs to save its results must persist reliably enough to retrieve the same result again. These are ensured by testing the code.

If you are using an external library of code, check the documentation about side effects: If you use a utility library to connect to a database, what does it do apart from inserting data when you ask for it to persist your classes? (Hint: nothing is the correct answer) Testing your own code by replacing the actual database with a fake database that mimics correct behavior ensures the correctness of your code. Testing your code along with a copy of the real database ensures the integration of all systems.

In your own code, consider separating the parts where computation is done and the parts where they are presented and the parts where they are persisted. Try to organize your code so that side effects are produced only in designated parts of it so that you know your computing functions are consistent and focused only on computing. This way, you can test for consistent results from your functions and handle chaos in your presentation and persistence functions.

TL;DR

Handling exceptions improperly or not handling them at all can cause attackers to exploit the incorrect state of code or disclose them sensitive information. The code must handle exceptions properly, catching and responding correctly as they occur. Writing tests that cover exceptions is a good way ensure the correctness. Logging conservatively also helps prevent leakage of sensitive information. Handling side effects start by isolating code with side effects and thinking of less tainted functions in terms of side effects.

References


  1. Impersonating a Named Pipe Client

  2. RevertToSelf function

  3. Exceptions tutorial in Oracle Java documentation

  4. Unchecked Exceptions - The Controversy

  5. Class NumberFormatException