What an attacker sees before they touch your site

Your API is answering errors with stack traces, SQL errors, and internal paths

When everything works, an API gives away very little. The interesting part is what it says when something breaks. A well-behaved endpoint catches the failure and returns a flat, generic message: something went wrong, try again. An endpoint running with development error handling does the opposite. It tells you exactly what went wrong, where in the code, on which file path, against which database, with the full trace attached.

That difference is the whole finding. A production API that answers errors verbosely is narrating its own internals to anyone who can make it fail, and making an API fail is easy: send a malformed parameter, an unexpected type, a value that breaks a query. The error page meant to help your developers debug is now helping a stranger map your application.

What an error response gives away

The richest leak is a raw stack trace. It hands over the file paths your code lives at, the framework and its internals, the call chain that led to the failure, and not infrequently a secret that happened to be in scope when the exception fired.

request
GET /api/order?id=abc HTTP/1.1 Host: app.yoursite.com
response
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{"error":"...","stack":"at OrderService.find
(/var/www/app/src/services/order.js:42:17)
at /var/www/app/node_modules/pg/lib/client.js:..."}
The trace hands over the file path, the framework, and the database driver in one failed request.

A database error is its own category. When the driver's message echoes through to the response, a SQLSTATE[...], an ORA-00942, a Postgres syntax error at or near, a Table 'x' doesn't exist, it leaks schema details, table and column names, and signals error handling that doesn't sanitize, which is exactly the handling that makes SQL injection easier to probe. A framework exception class serialized into the body, a Laravel Illuminate\..., a Django django.core.exceptions, a Symfony component, a .NET exception type, tells an attacker the stack and confirms debug mode is on even when the full trace is suppressed.

The smaller leaks add up the same way. An absolute server path like /var/www/... or C:\inetpub\... reveals the deploy layout and the OS user. A private IP or an .internal hostname in the body is a sketch of the network behind the app. A route or middleware array serialized in an error hands over a map of the application's endpoints. A debug flag returned alongside query logs and timing is the development profile shipping its internals to the client. And a X-Powered-By or x-aspnet-version header on the response is a version banner that points an attacker straight at the matching CVEs.

0:00Attacker sends a malformed parameter
0:05API returns a 500 with the full stack trace
0:20Reads file paths, framework, DB driver
0:45Probes the database error for an injection point
Each verbose error is a free answer to a question an attacker would otherwise have to guess.

Why production ends up verbose

Almost always, because nobody turned debug off. Every major framework ships two error modes: a detailed one for development and a quiet one for production. The detailed one is the default while you build, because you want the trace. Flipping to production handling is a single setting, APP_DEBUG=false, DEBUG = False, the production profile, and a single setting is an easy thing to never get to before launch.

Sometimes it is a custom error handler that was written to be helpful and forwards the underlying error into the JSON response on purpose, never reconsidered once the app went live. Either way the result is the same: the failure path, the one part of the API a developer rarely looks at from the outside, is the most talkative endpoint you serve.

Turn on production error handling

The fix is to make every failure return a generic body and keep the detail in your server logs, where it belongs.

# Laravel  (.env)
APP_DEBUG=true     # full trace in the response

# Django  (settings.py)
DEBUG = True       # detailed error page in the body
Generic message to the client, full detail to the server log. One setting in most frameworks.

Switch the framework to production error handling so exceptions, database errors, and traces are logged on the server and never serialized to the client. Catch database errors and return a flat message rather than echoing the driver's. Strip absolute paths, internal hosts, and route dumps from anything client-visible, and drop the X-Powered-By and version headers from your responses. The detail you want when debugging is exactly the detail an attacker wants when targeting you; the only safe place for it is your logs.

Whether your API narrates its internals on failure is visible from outside, because a verbose error is just a response, so that is how SurfaceCheckr checks it. During a real-browser render it watches the responses your own page receives and flags the ones that carry a stack trace, a raw database error, a serialized framework exception, an absolute server path, internal network detail, a route or middleware dump, a debug flag with its corroborating query logs, or a version-banner header. It reads the responses your page already requested and never sends a malformed request of its own to force an error, because it stays passive, and the evidence it keeps is redacted to a snippet. What an attacker would have to provoke, your page sometimes surfaces on its own. The check is making sure your error path isn't the most informative thing you serve.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.