Your API is leaking secrets through verbose errors
By Seaworthy · 10 October 2026
Photo by Caleb Martin on Unsplash
Your API is leaking secrets through verbose errors
I get it. You asked your AI assistant to scaffold a REST endpoint, and it worked on the first try. Tests passed. The response shape looked clean. You moved on to the next ticket.
Now open that endpoint in a browser and send it garbage. What comes back?
If the answer includes a stack trace, a SQL fragment, a file path, or the name of your internal service, you have a data exposure bug. It is one of the oldest ones in the book, and AI coding assistants are quietly making it worse because they optimize for a working demo, not a hardened deployment.
Why this keeps happening
Verbose errors are useful during development. A framework like Express, Django, or Spring will, by default, print a full trace so you can debug. That default is fine on your laptop. It is not fine in production.
The problem is that the same app.js or settings.py often moves from laptop to server with almost no changes. Debug mode stays on. Error handlers return err.stack. And because the response is a 500, your monitoring might not even flag it as unusual.
AI assistants compound this. When you prompt "build me a user lookup endpoint," the model produces something that works. It does not usually ask "do you want to run this in production?" or "should I strip internal details from errors?" You have to ask for that. Most people do not, because the demo already works.
What actually leaks
A verbose error can reveal:
- File system paths (
/home/deploy/app/services/user.js:42) - Database type and version (
SQLSTATE[42S22]: Column not found) - Internal hostnames and ports (
redis-cache-01:6379) - Framework and library versions (useful for matching known CVEs)
- Business logic hints (which fields exist, which queries run)
None of these are catastrophic alone. Together they shorten an attacker's reconnaissance from days to minutes. That is the part that bothers me. We treat error messages as a debugging convenience, but to someone probing your API, they are a free tour of your architecture.
A pattern you have probably written
Here is the shape I see most often. It looks harmless.
app.get('/api/user/:id', async (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(user.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message, stack: err.stack });
}
});
The stack field is the problem. So is err.message when it comes straight from the database driver, because drivers love to echo query text and column names.
The fix is boring, which is why nobody does it
You want two different behaviors: loud errors for you, quiet errors for clients.
app.get('/api/user/:id', async (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(user.rows[0]);
} catch (err) {
// Full detail goes to your logs, not the response.
logger.error({ err, path: req.path, requestId: req.id });
// Client gets a generic message and a correlation id.
res.status(500).json({
error: 'Internal server error',
requestId: req.id
});
}
});
The requestId is the useful part. When a user reports a failure, you search your logs for that id and see the real stack trace. The client never sees it.
Some frameworks make this easy. Express has NODE_ENV=production behavior, but only if you let the default error handler run. Django has DEBUG = False, which changes error pages but not custom views. Spring Boot has server.error.include-stacktrace=never. Check your framework. Then check it again in the environment that actually serves traffic.
The AI assistant angle
If you use an AI coding assistant, add one line to your project instructions or system prompt: "Never return stack traces, raw exception messages, or internal identifiers in HTTP responses. Log them server-side and return a generic message with a request id."
That single sentence prevents a whole class of bugs before the code is generated. It is cheaper than auditing every endpoint later.
You should also stop trusting that your assistant will remember this across sessions. Put it in a linter rule if you can. A grep for err.stack in response objects is ugly, but it catches real problems. If you want something less crude, write a test that sends a malformed request to every route and asserts the response body does not contain the strings stack, at , or your project root path.
My honest take
Verbose errors are a symptom of a larger habit: treating development convenience as a production default. AI assistants make that habit easier because they remove the friction of writing boilerplate. Friction is sometimes what forces you to think about where code runs and who sees the output.
I am not asking you to turn off debugging. I am asking you to be deliberate about the boundary between what you log and what you return. That boundary is a security control. Treat it like one.
Seaworthy provides a security review service for teams that want this kind of check without building an internal audit program from scratch.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.