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

Deployment metadata files exposed: vercel.json, netlify.toml, fly.toml

When the project root is your webroot, files like vercel.json and netlify.toml get served to anyone. This note shows builders how to check for them and what their contents hand an attacker before launch.

ExposureDeployment configRecon

A public GET to vercel.json is recon, not a breach

Before you share the launch URL, check whether your deployment config files answer a plain HTTP request. The specific risk: a request to /vercel.json, /netlify.toml, /fly.toml, /render.yaml, or /railway.json returns the file's contents to anyone, instead of a 404. None of these is as severe as a public .env. They rarely hold secret values. What they hold is a map of your app, and that map makes every other attack cheaper.

Keep the two states separate. Observable is the fact you can verify right now: the file returns a 200 with parseable config. Possible is what an attacker does next with the contents, which depends on what else is exposed. This note is about the observable part, because that is the part you can fix before launch.

  • The finding is information disclosure (CWE-200 territory), not remote code execution.
  • It is reconnaissance: it shortens the time an attacker spends guessing your routes, header rules, and environment variable names.
  • It is usually medium severity on its own, and it raises the severity of whatever else you left exposed.

What each file reveals, and what it does not

These files describe how your app is built and routed, not what data it stores. The distinction that matters most: they expose the names of environment variables, not usually the values. A name like STRIPE_WEBHOOK_SECRET or INTERNAL_ADMIN_URL is not a secret by itself, but it confirms which integrations you run and where the soft spots are.

Here is roughly what an attacker reads from each one.

What they do not give up: the actual secret values, your source code, or your database contents. A vercel.json that lists an env key called DATABASE_URL has not leaked the URL. It has told the attacker you have a database and what you call its connection string, which is exactly the kind of hint that turns a blind guess into a targeted one.

  • vercel.json: rewrites and redirects (your real internal path structure behind pretty URLs), header rules, function and route names, and any env mapping declared in the file. Rewrites are the prize, because they reveal endpoints you never linked publicly.
  • netlify.toml: build command and publish directory, redirect and rewrite rules, header blocks, and the names of edge or serverless functions. Redirects with a 200 status often expose a proxied backend origin.
  • fly.toml: the internal port your app listens on, configured services and health-check paths, mounts, and process groups. It sketches your runtime topology.
  • render.yaml and railway.json: service definitions, start commands, declared env var keys, and how services connect to each other.

Exposed variable names are not credentials. Treat them as a hint sheet: confirmation of your stack, your integrations, and the named paths worth probing. The damage is in combination with another exposure, not in the name alone.

Why AI-built apps serve these files

The root cause is almost always the same: the project root is being used as the webroot. The deploy step serves the whole directory the code lives in, instead of serving only the built output. When that happens, every file sitting next to your code, including the deployment config, is reachable over HTTP.

This pattern shows up in fast AI-generated builds for a few specific reasons.

The platforms themselves usually handle this correctly: Vercel and Netlify consume their config at build time and do not serve it. The exposure tends to appear when you self-host the same project, run it behind a generic static server, or containerize it without separating build output from source.

  • The config file lives in the project root by design (that is where the platform expects it), and a misconfigured static server happily serves the root.
  • A generated Dockerfile or static-host config copies the entire project directory into the served path rather than copying only dist or build or public.
  • The app works on the first deploy, so nobody opens the public URL and requests the config filenames. The file is invisible from inside the running app.

Check the filenames on your own URL

You can confirm this from outside with no attack and no credentials. Request the common config filenames against your own production URL and read what comes back.

The filenames worth checking: /vercel.json, /netlify.toml, /fly.toml, /render.yaml, /render.yml, /railway.json, /railway.toml, /app.json, /Procfile, and /Dockerfile. You can do this in a browser address bar or with a quiet curl from your own machine, against your own domain only.

After you find one, read it manually. The scan tells you the file is reachable. Deciding which line in it is sensitive (a rewrite to an internal admin route is worse than a cache header) is a quick human read.

  • Suspicious result: a 200 response whose body is parseable config. If GET /vercel.json returns JSON with rewrites and headers, or GET /netlify.toml returns TOML with build settings, the file is reachable and you have the finding.
  • Safer result: a 404, a 403, or your app's own HTML fallback page (a single-page app often returns index.html for unknown paths). HTML that is clearly your app, not config, is the expected deny.
  • Watch the content type as well as the status. A 200 that serves application/json or text/plain config is the problem; a 200 that serves your SPA shell is not.

Serve only the build output, then deny the config

The durable fix removes the root-as-webroot mistake. Everything else is defense in depth on top of it.

Serve only your build output, never the project root. Point your static host or container at dist, build, or public, and confirm the deployment config files are not inside that directory. If a file does not live in the served path, it cannot be served. This single change closes the whole class, not just the filenames you remembered to list.

Then add explicit deny rules as a backstop, in case the served path and the project root ever overlap again. Most servers and CDNs let you return 404 for requests matching these filenames. Keep the rule by name and extension so a new platform file (a future railway.json, say) is covered by the pattern rather than missed.

  • Build artifacts go in the served directory; source and config stay out of it.
  • Add a deny or 404 rule for the deployment config filenames at the server or CDN edge.
  • Treat any exposed variable names as recon already in the wild: assume an attacker knows your integration list and the named paths, and make sure those paths are actually authorized server-side.

Removing the exposed config does not secure the routes it described. If vercel.json revealed a rewrite to /admin-internal, the fix is to require auth on that route, not just to stop serving the file. The config leak made the route easy to find; an authorization check is what stops it from being reached. Authentication confirms who is calling; authorization decides whether that caller may hit the route at all, and that is the part you must verify in code.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check fetches the common deployment config filenames against your public URL and reports any that return a 200 with parseable config content. It tells you the file is reachable from outside, with severity and fix direction, so an exposed vercel.json or netlify.toml shows up in your scan rather than in someone else's recon notes. It checks the thing an outside visitor can actually request.

What it does not do is judge which lines inside the file are sensitive for your specific app, or fix the routes those lines describe. Reading the contents and deciding whether a given rewrite, header, or variable name matters is a quick manual review, and closing an internal route the config revealed is a code change you make and re-verify yourself.

Next step: request /vercel.json, /netlify.toml, and /fly.toml against your own production URL right now. If any returns config, move it out of the served directory and add a 404 rule, then rerun a focused Launch Check before you share the URL publicly.

A clean Launch Check here means the config filenames are not reachable from the public surface. It does not prove your routes are authorized, your secrets are safe, or that no config leaked through some other path such as an exposed .git directory or a public .env. Treat a returned config file as a hardening task before launch, and treat a clean result as one signal among several, not a guarantee.

> launch check

Scan the public surface before launch.

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