A security review of a Laravel application reported a critical finding: remote code execution in attachment handling. The finding was wrong. What we discovered while disproving it was worse in its own quiet way — every upload was failing, and nobody knew.
Two Trees, One Catch
Since PHP 7 there have been two separate trees of things that can be thrown: Exception for expected failures and Error for programming faults — calling a method that does not exist, a wrong type, exhausted memory. All they share is Throwable. A catch (Exception $e) block catches only the first tree; an Error passes straight through it as if it were not there.
How the Upload Died
A metadata helper called a method of the file-storage library that had been removed in a newer version. Calling a method that does not exist throws an Error. The controller wrapped attachment handling in a tidy catch (Exception) with a message for the user — which never saw the Error at all. The result: a 500 on every upload, no entry in the application log, no message to the user. Reports with attachments simply vanished.
The False Finding That Led to the Real One
A few lines further down, the same class had a call that assembled a shell command — on paper a textbook remote code execution. The review rated it that way. But the line was dead: execution never reached it, because that very Error a few lines above aborted the request every single time. The lesson we wrote down: reachability is proven line by line, not by tracing calls up to the function.
What We Changed
The dead code went; the helper was rewritten without assembling commands. At the system’s boundaries — where a controller decides what the user sees — we catch Throwable, not Exception, and report every such case to the log before returning a response. Catching everything is no cure for bad code, but it guarantees the failure is at least visible. Dying quietly is the worst way to die.
An error nobody sees is not handled — it has just been passed on to the user.