OWASP Top 10:2025 — A10 Mishandling of Exceptional Conditions for AI-Generated Code

Written by the Rafter Team

A user uploads a 600 MB video to your AI-built SaaS. The route handler — written by Claude Code last Tuesday, never re-read — wraps the parse in a bare try block that catches Exception, swallows it, and returns 200 OK with { status: "processing" }. The job is silently dead. Worse, the temp file never gets cleaned up. Two weeks later your disk is full, your background worker is OOM-looping, and the only signal is users complaining their videos never finished. That is A10. It's the most boring entry in the new Top 10, and it's the one your LLM is most likely to ship with a smile.
What A10 actually is
A10 covers everything that goes wrong when your code meets a condition it wasn't written to handle: unhandled exceptions, null/undefined dereferences, resource exhaustion (unbounded loops, leaked file handles, runaway DB connections), and error paths that leak state back to the user (stack traces in JSON responses, internal IDs in 500 errors). One concrete example: a payment webhook handler that throws on a malformed Stripe payload and returns a 500 with the full traceback. The traceback names your internal queue, your DB host, and the version of your ORM. An attacker now knows your stack without scanning a single port.
Why AI-generated code trips on it
LLMs are trained on Stack Overflow answers and tutorials. Tutorials show the happy path. So the code you get back is fluent, idiomatic, and almost completely missing its error edges. Three patterns we see constantly in scans of vibe-coded apps:
- The blanket catch.
try { ... } catch (e) { console.log(e) }— or worse, an empty catch. The route returns success, the error vanishes into stdout, the user thinks it worked. Common in Express, FastAPI, and Next.js route handlers. - The optional-chaining lie.
user?.profile?.email.toLowerCase()looks safe but the.toLowerCase()at the end still throws ifemailisnullvsundefined. LLMs love optional chaining and frequently misplace where the chain ends. - Unbounded resource use. A loop that reads a file line-by-line into memory, a
while (hasMore)pagination loop with no max, a Supabase query with no.limit(). Fine on dev data, a denial-of-service vector in production.
Next.js App Router is especially bad here — server actions and route handlers default to returning the raw thrown error in dev and a generic 500 in prod, so devs never see the error shape that ships. Python apps built on FastAPI tend to inherit the framework's default exception handler, which is fine, until the LLM "improves" it with a custom one that re-raises with repr(e) in the body.
The fix on agentic CLIs (Claude Code, Codex)
You have direct file access. Use it. After any LLM-generated route handler, background job, or external API call, run a single review pass focused on the error edges. Paste this:
Audit this file for OWASP A10 (mishandling of exceptional conditions). For
every try/catch, every await, every external call, every loop, and every
deref of a possibly-null value, tell me:
1. What happens if it throws?
2. What does the user see — and does it leak internal state (stack trace,
file path, DB host, library version)?
3. Is there a resource (file handle, DB connection, temp file, lock) that
leaks on the error path?
4. Is there an upper bound on iteration, memory, or recursion?
Then propose a diff that adds bounded handling, structured error responses
(no traceback in the body), and resource cleanup via finally / using /
defer. Do not catch and ignore.
For anything Rafter flags as A10 in a scan, the Copy-for-AI button packages the file, the finding, and a fix-ready prompt — paste it straight into Claude Code or Codex and apply the diff. Don't accept the first patch blindly; LLMs will frequently "fix" an unhandled exception by adding the blanket catch we just told you to avoid. Re-read the diff.
The fix on opinionated platforms (base44, Greta, OpenClaw)
You usually can't edit the generated handler directly, so you have to make the platform agent do it. Two things to bake into your standing instructions for the project (most builders have a "rules" or "project context" panel — use it):
- A global error contract. Tell the platform: "Every API endpoint must return errors as
{ error: { code, message } }with no stack traces, no internal IDs, no library names. Log the full error server-side; never return it to the client." Re-paste this rule when you ask for any new endpoint. - Resource and iteration limits. "Every database query must have an explicit limit. Every loop over external data must have a maximum iteration count. Every file upload must enforce a size cap before reading into memory. Every async job must have a timeout."
Then, for each feature you ship, prompt explicitly: "What happens when the input is malformed, the third-party API times out, or the user is offline mid-request? Show me the error path." If the platform can't answer concretely, it didn't write one — and you're shipping A10. Most opinionated builders will happily regenerate the handler with proper error wrapping if you name the requirement; they just won't do it unprompted.
The platform's marketing implies the runtime catches everything for you. It catches crashes. It does not catch leaked stack traces, exhausted file descriptors, or silent data loss. Those are yours.
See also
- A06 Insecure Design — most A10 bugs are A06 bugs that grew up. If error handling wasn't designed in, it wasn't generated in.
- A09 Security Logging and Alerting Failures — swallowed exceptions are invisible exceptions. A10 and A09 are the same failure seen from two sides.
- A02 Security Misconfiguration — leaking stack traces in production responses is the canonical misconfiguration that makes A10 exploitable.