Skip to content
Back to field notes
SupabaseJune 6, 20266 min read

Supabase Edge Function secrets: what leaks when error handling is missing

An AI-generated Supabase Edge Function that returns its raw error on a crash can hand a visitor your env var names and internal service URLs. This note shows how to check what your function returns before you share the URL.

SupabaseEdge FunctionsSecrets

A crashed function can return its own error to the caller

A Supabase Edge Function is a Deno function on a public URL, usually under /functions/v1/your-function. The concrete supabase edge function secrets exposure risk is narrow and direct: when an uncaught exception bubbles up and the handler returns that error object or its stack trace in the HTTP response, the caller reads internals that were never meant to leave the server.

Separate what is observable from what is only possible. What an outside caller can observe is the response body your function actually sends back. If that body contains a stack trace, a thrown error message, or a serialized error object, anyone who can trigger the error path sees it. What is only possible is what those internals reveal next. A stack trace can include file paths and line numbers. An error from a failed fetch can include the internal service URL it was calling. A database error can echo a query or a column name. None of that is the secret value itself, but it is a map of where the secrets live and what the function talks to.

  • Deno exposes secrets to the function through environment variables, read with Deno.env.get. The values do not normally appear in a stack trace, but the variable names and the URLs they point to often do.
  • A thrown error from a downstream call (a database, a payment API, another internal endpoint) frequently carries the target URL or host in its message.
  • This is an information-disclosure problem, in the OWASP and CWE sense of returning sensitive technical detail in an error. It is not, by itself, the same as the key value being printed, and it is worth being precise about that difference.

Why AI-generated edge functions land here

The fast path an AI assistant writes is a handler that does the happy-path work and returns a result. Error handling is the part that gets skipped, stubbed, or written to be maximally informative during development.

A common generated shape is a try/catch where the catch block does return new Response with the error serialized into the body, often with JSON.stringify(error) or error.message, so the developer can see what went wrong. That is genuinely useful while building. It is also exactly the line that ships to production unchanged, because the function works on the happy path and nobody re-opens the catch block before deploy.

The result is a function that is fine until the first real error, at which point it answers a stranger with its own internals.

  • The function returns correct data on a normal request, so it looks finished from the outside.
  • The catch block only runs on a failure, so the leaky response never appears during a quick manual test.
  • Edge Functions deploy fast and get a public URL immediately, so the endpoint is reachable before anyone reviews what it returns when a dependency is down or an input is malformed.

Check what your function returns on failure

You can confirm this on your own function without attacking anything. The goal is to see the error path, which a normal request will not show you.

First, find the function URL. It is under your project at /functions/v1/your-function. Then send requests that are likely to trip the error path, and read the full response body, not just the status code. Use your browser DevTools Network tab or a command-line client, and look at the raw body.

Also read the deployed code, not just the response. Open the function source and look at every catch block and every place that builds an error Response. If any of them put error, error.message, error.stack, or JSON.stringify(error) into the response body, that is the line to change. Checking the response only proves the paths you managed to trigger; reading the code is how you find the ones you did not.

  • Send a request with a missing or malformed body when the function expects JSON, an empty payload, or a wrong content type. Functions that parse input without guarding it often throw here.
  • Send a request that forces a downstream failure if you can do so safely on your own project, for example an input that makes a dependent lookup miss.
  • Suspicious result: the body contains a stack trace, a file path, a variable name like SUPABASE_SERVICE_ROLE_KEY or STRIPE_SECRET_KEY, an internal host or URL, a database error, or a serialized error object with a stack field.
  • Safer result: the body is a generic message such as a plain "Internal server error" with a 500 status, and the status code carries the signal while the body says nothing about internals.

Return generic errors, log the detail server-side

The fix is to separate what the caller sees from what you keep. The caller gets a generic message and a status code. The full error goes to your logs.

In each catch block, log the real error with console.error so it lands in the Supabase function logs, then return a Response with a fixed, non-descriptive body and the right status. The caller learns that something failed, not what or where. Keep the detail on the server side where you can read it and the public cannot.

What this fix does not cover: it stops the error body from leaking internals, but it does not fix whatever caused the error, and it does not address whether the function is doing the right authorization check in the first place. A function that returns a clean generic error can still be returning data to a caller who should not have it. Information disclosure in the error path and a missing authorization check are two separate problems; fixing one does not fix the other. It also does not retroactively rotate any secret that already leaked. If a real key value was ever returned or logged in a place that was exposed, treat it as compromised and rotate it.

  • Return a stable, generic body for all failures (for example a short message and an error code you define), never the caught error.
  • Log error and error.stack with console.error inside the function; those entries are visible to you in the dashboard logs, not to the caller.
  • Validate and parse input at the top of the handler so predictable bad input returns a controlled 400, instead of throwing into a catch that then leaks.
  • Keep secrets in Deno.env and never interpolate a secret into any string that could reach a response or a log line that is exposed.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check hits your public Edge Function endpoint and inspects the response body. When that body contains a stack trace or secret-shaped strings, such as known env var name patterns or internal URLs, the scan flags it with severity and fix direction. It is checking the thing an outside caller can actually reach: what your function says back over HTTP.

Be clear about how strong that signal is. This is a weak public-surface signal only. The scan can flag an error response it manages to trigger, but it cannot exercise every code path in your function, and a function can return a clean response on the requests the scan sends while still leaking on a path it did not hit. Confirming that error handling is correct across all branches needs a code review of every catch block and every error Response, which is work the scan does not do. The scan also does not judge whether the function's authorization is correct.

Next step: open each Edge Function, find every catch block, and make it log the real error and return a generic body. Then run a focused Launch Check against the public URL before sharing it, so a leaky error path shows up in your scan and not in someone else's request log.

A clean Launch Check on a function endpoint does not prove the function is safe. It proves the responses the scan saw did not leak; it does not read your code or guarantee that a different input would not surface a stack trace. Treat a flagged response as a launch blocker, and treat a clean result as one signal, not a verdict.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.