Uncaught exception
Catch 'em all: protect your app from uncaught exceptions
JavaScript
What is an uncaught exception?
Uncaught exceptions are a common problem in JavaScript programming that can cause unexpected crashes and other types of vulnerabilities in web applications. These errors occur when an exception is thrown but not properly handled by the code, leading to undefined behavior and potential security vulnerabilities. To prevent these issues, you should always handle returned errors, use error-handling techniques such as try-catch blocks, and implement data validation to prevent input errors.
About this lesson
In this lesson, you will learn how uncaught exceptions work and how to protect your applications against them. We will begin by exploiting an uncaught exception vulnerability in a simple application. Then we will analyze the vulnerable code and explore some options for remediation and prevention.
What actually happened? Why did we get an error message exposing an API key?
Let's look at the code behind the /waitlist
endpoint.
As you can see, the waitlist endpoint performs a request to the API of the website. At first sight, this code looks rather good. But how did you and William manage to leak the API key?
The problem sits within the error handling of the request. Or actually, the problem is that there is no error handling at all.
The API URL is constructed using the host-header from the request. The reason is that the code has to work out of the box on multiple domains (target.xyz
, target.abc
, etc.). This is common practice, but not entirely recommended, as the host-header is essentially user input.
Since no validation is done on this value, we can craft an invalid URL and trigger an exception from within the request function. This exception is then, by default, written to the response, exposing the target.net API key within the request URL that was supposed to stay private.
The difference between supplying an empty host-header in this scenario vs a host header containing a non-existing host is the following:
Using a host-header with the value “i-do-not-exist.abc”, the application tries to make a request to “i-do-not-exist.abc” which results in a DNS resolution error:
events.js:292 throw er; // Unhandled 'error' event ^Error: getaddrinfo ENOTFOUND i-do-not-exist.abc at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)Emitted 'error' event on Request instance at: at Request.onRequestError (/home/target/node_modules/request/request.js:877:8) at ClientRequest.emit (events.js:315:20) at TLSSocket.socketErrorListener (_http_client.js:432:9) at TLSSocket.emit (events.js:315:20) at emitErrorNT (internal/streams/destroy.js:84:8) at processTicksAndRejections (internal/process/task_queues.js:84:21) { errno: -3008, code: 'ENOTFOUND', syscall: 'getaddrinfo', hostname: 'i-do-not-exist.abc'}
The application terminates because this is an uncaught exception, that isn’t being caught by Express.
However, when we use an empty value as host-header, we construct an invalid URL, and an error is thrown that is actually caught by Express. Just not by the custom target.xyz
code, but rather by default Express code, which decided that it is best to print it to the response as well as to stderr
.
Because target.xyz
didn’t handle errors, an exception slipped through which, unintendedly, exposed sensitive information that was deemed private and secure.
Impacts of an uncaught exception
The impacts of uncaught exceptions depend on what exception is thrown and in which context. That is why the impact is generally called “unintended behavior”.
Below are some examples of this unintended behavior.
Cross-site scripting
User input within uncaught exceptions are not filtered or sanitized. For example, a function that converts an integer as string to an integer of type int32.
If the string is user-supplied and contains <h1>Hello World</h1>
, then the convert function will throw an error saying that <h1>Hello World</h1>
is not a number.
Denial-of-service
Most of the time, uncaught exceptions end up crashing the application. Even if an exception can be caught by a global exception handler, continuing running is not advised and can lead to undefined behavior.
Information exposure
As demonstrated in the scenario with William and target.xyz
, uncaught exceptions can expose sensitive information. This can include system information that can be used in further attacks but also personal information. For example, information about other users that are using the same application.
Preventing uncaught exceptions in JavaScript has been getting easier and easier over the years. Many functions in libraries now return error variables that contain a possible error that happened within the function.
For example, in the waitlist code of target.xyz
, the request function can be extended to handle errors:
This checks the error variable for a possible error and then handles the error without showing the stacktrace to the user.
If this error variable is not available, a try…catch
statement can be used to catch errors happening within the request function:
Even though this seems like a solution that would catch all errors, this isn’t true. The request function is an asynchronous function that returns immediately and does not throw an error when it encounters an invalid host. Instead, it triggers an "error" event on the returned request object.
To catch this error, we need to add an event listener for the "error" event on the request object and handle it appropriately.
Luckily, these last two solutions are not needed in the scenario of target.xyz
because the first solution is sufficient enough in catching all types of errors stemming from the request function.
While the errors are now handled, it would be even better to not have those errors occur in the first place. Input validation could have played a big role here in preventing the API from leaking.
For example, the domains on which the application would run were known beforehand (target.xyz
, target.abc
and target.lmn
). It is also known that the host-header should always contain a value and can not be empty.
Implementing a simple hostname whitelist for the host-header would result in no errors being thrown from the DNS resolution and no errors being thrown from the URI parser.
Of course, unexpected errors such as a timeout from the API can still happen. That is why input validation alone isn’t enough.
Test your knowledge!
Keep learning
To learn more about uncaught exceptions, check out more about this CWE here: https://cwe.mitre.org/data/definitions/248.html