CWE-248: Uncaught Exception
Overview
An uncaught exception occurs when an operation raises an error condition that no calling code intercepts, letting it propagate to a boundary the application never intended to expose - a crashed process, or a raw stack trace returned in an HTTP response. An unhandled promise rejection is the same failure on the asynchronous path. Beyond the immediate denial of service, the default error output often leaks implementation details an attacker can use for further reconnaissance.
Relationship to Other CWEs
CWE-248 is a child of both CWE-705 (Incorrect Control Flow Scoping) and CWE-755 (Improper Handling of Exceptional Conditions), which puts it under two pillars at once. Of its four ancestors only CWE-691 has a page here, and it is a Pillar MITRE discourages mapping to. CWE-248 itself is Allowed for direct mapping, so it is the number to file.
- CWE-248 (this page) - an exception that escapes uncaught and terminates the thread or process
- CWE-691 (Insufficient Control Flow Management) - the pillar reached by way of CWE-705. The other, CWE-703 (Improper Check or Handling of Exceptional Conditions), is reached by way of CWE-755 and has no page here
- CWE-252 (Unchecked Return Value) - where the failure is signalled by a return value nobody reads rather than by an exception nobody catches
- CWE-209 (Generation of Error Message Containing Sensitive Information) - where the exception is caught but its contents reach the caller. That page covers what a safe error response may contain in far more detail than the Sanitize Errors Returned to Callers section below
- CWE-391 (Unchecked Error Condition) - planned for deprecation, and MITRE splits it three ways rather than routing it all here: an exception that escapes uncaught is this page, a return value nobody reads is CWE-252, and an exception caught into an empty block is CWE-1069 (Empty Exception Block). The last is the swallowed case and is not this page. Check which of the three a CWE-391 finding actually describes before re-filing it
OWASP Classification
A10:2025 - Mishandling of Exceptional Conditions
Risk
Medium: Uncaught exceptions crash processes or threads (denial of service), leave resources such as file handles, sockets, and database connections unclosed, and abort operations partway through so data is left in an inconsistent state. When the default error handler renders exception details back to the caller, they also disclose stack traces, file paths, class names, and query fragments that help an attacker map the application's internals.
Remediation Steps
Core Principle: Every code path that can fail must have a defined handler; nothing should be able to escape uncontrolled to the process boundary or into an HTTP response.
Trace the Data Path
- Source: Any operation that can fail - parsing, file/network I/O, external API calls, type coercion, arithmetic on untrusted values
- Sink: The point where the exception either crashes the process/thread or is serialized into a response body
- Missing control: No handler between the failing operation and that sink, or a handler that logs but still lets the response include the raw error
Install a Global/Boundary Handler (Primary Defense)
- Register one handler at the request boundary - framework error middleware, a servlet filter, a controller advice, a wrapped
mainfor a batch job - that catches anything not handled locally. This is the handler that can still answer the caller, because the request is alive when it runs - The request-boundary handler logs full detail server-side and returns a generic, fixed-shape error to the caller - it never re-serializes the raw exception or its message
- A process-level hook is a different thing and does not replace it.
process.on('uncaughtException'),Thread.setDefaultUncaughtExceptionHandlerandAppDomain.UnhandledExceptionrun after the stack that owned the request has already unwound, so there is nothing left to write a response onto: measured on Node 24.3, aJSON.parsefailure inside anhttprequest listener runs theuncaughtExceptionhandler and leaves the client waiting on a socket that is never answered. Use the process hook to log the failure and shut down deliberately, not to keep serving - Node's own documentation callsuncaughtExceptiona crude mechanism and warns that the process may be in an undefined state once one has fired, and .NET'sAppDomain.UnhandledExceptioncannot prevent the termination it is notifying you about - Register the equivalent handler for asynchronous code: an unhandled promise/future/task rejection is the async analogue of an uncaught exception and needs the same treatment
- Treat the boundary handler as a safety net rather than the only one - handle expected failures (bad input, not-found, timeout) close to where they occur so the response can be specific and correct
Release Resources on Every Exit Path
- Use the language's scope-bound release construct (try-with-resources,
using, a context manager, RAII,defer) so a connection, file handle, or lock is released whether the block completes normally, returns early, or throws - Do not rely on cleanup code written after the risky operation - if that operation throws, the cleanup code never runs
Sanitize Errors Returned to Callers
- Return a generic message and an appropriate status/error code; log the full exception (message, stack trace, request context) only to server-side logs
- Never let the default framework error page, or an unmodified
catchblock, re-render the exception's message or stack trace into the response - Disable development/debug error pages in production - they exist specifically to show what production error handling must hide
Test with Failure Conditions
- Send malformed input the parser cannot accept (invalid JSON, oversized payloads, wrong content type) and confirm the response has no stack trace or internal paths
- Force a downstream failure (unreachable database, timeout, denied file permission) and confirm the connection/handle is released rather than leaked
- Trigger an async operation's failure path and confirm the rejection is caught rather than reaching the runtime. Check what your runtime does with one before deciding how loud that test needs to be: Node has terminated the process on an unhandled rejection since v15 - measured on 24.3, a bare
Promise.rejectprints the trace and exits 1 - so on Node 18 and later the symptom is a dead worker, not the warning older documentation describes - Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - no handler between a failing operation and the caller
function handleRequest(input):
data = parse(input) // throws on malformed input
return renderResponse(data)
// Attack: send malformed input
// Result: unhandled exception - process crash or raw stack trace in the response
Why this is vulnerable: the exception is the correct behaviour of parse and the defect is the absence of anything that expects it. handleRequest instead delegates the response to the runtime, and the runtime's default is written for a developer at a console: a stack trace naming every frame, file path, framework and version in the call chain, or a process that stops. Neither is a decision the application made about what to tell an anonymous caller.
Both outcomes are useful to an attacker for different reasons. The trace is reconnaissance that survives fixing whatever threw, describing the deployment rather than the error. The crash is availability: if malformed input reliably terminates the handler, a request costing nothing to send removes a worker, and where the failure kills the process rather than the request, a small number of them takes the service down. That is why "it only crashes on invalid input" is not mitigation - invalid input is the part the attacker controls completely.
Secure Patterns
// SECURE - local handling for expected failures, generic response on the way out
function handleRequest(input):
try:
data = parse(input)
return renderResponse(data)
catch ParseError as e:
log.error("parse failed", e, requestContext)
return genericError(400, "Invalid request")
// Boundary handler catches anything not handled locally
onUnhandledError(e):
log.error("unhandled exception", e)
return genericError(500, "An error occurred")
Why this works: Expected failures are handled where enough context exists to respond correctly; anything unexpected is still caught by the boundary handler before it can crash the process or leak the exception's contents, so no failure path reaches the client unfiltered.
Common Pitfalls
- Catching
Exceptionand logging only: The catch block logs the error but still lets execution fall through to code that assumes success, or returns the caught exception's own message - the crash is prevented but the inconsistent state or information disclosure remains. - Handling the synchronous path but not async: Try/catch is added around synchronous code while a promise chain, async task, or callback error path is left without a rejection handler - the exception still escapes uncaught, just on a different execution path.
- Disabling the debug error page without adding a real handler: Turning off the framework's stack-trace page in production without installing a boundary handler still lets the process crash or return a blank/opaque error, instead of failing gracefully with a proper status and logged detail.
- Wrapping the risky call but not the cleanup: A try/catch is added around the operation that can throw, but the connection or file is still closed after the try block instead of in a
finally/scope-bound construct - an exception still skips the cleanup.