Sourcemap leaks in vibe-coded apps: how attackers reconstruct your codebase from a .map file.

2026-05-09 · vibecheck team · 8 min read · Build · Sourcemaps

Quick answer Production source maps de-minify your bundle into the original TypeScript / TSX with comments, file structure, and dead code intact. Three minutes of an attacker's time — view-source, find //# sourceMappingURL=, fetch the .map, run source-map-cli decode — and your repo is on their disk. The specific damage depends on what's in the source: leaked secrets compiled in, internal API URLs you didn't intend to publish, business-logic comments, recently-deleted-but-still-in-history dead code containing rotated credentials. The fix is universal: turn off sourcemap emission to public URLs in your bundler config, OR switch to "hidden" mode (emit but don't link from the bundle) and upload to your error-monitoring tool. Per-bundler configs below.

Source maps are the most underestimated leak in vibe-coded apps. They're the silent default in every modern bundler. They produce a file that — accidentally or otherwise — ships to your public URL alongside the minified bundle, and that file contains everything in your src/. Search-engine crawlers index them. AI-coding agents fetch them when fingerprinting an app. Every static-analysis tool from vibecheck to TruffleHog scans for them.

This post walks the attack, the per-bundler fix, and the three exceptions where you actually want sourcemaps in production.

What's in a .map file

A source map is a JSON document. It looks like this:

{
  "version": 3,
  "sources": [
    "src/main.ts",
    "src/lib/supabase.ts",
    "src/components/Login.tsx",
    "src/utils/auth.ts",
    "../node_modules/zod/lib/index.mjs",
    ...
  ],
  "sourcesContent": [
    "import { createClient } from '@supabase/supabase-js'; ...",
    "// Note from Mike: the prod URL needs the suffix-_v2 ...",
    ...
  ],
  "names": [...],
  "mappings": "AAAA,..."
}

sources is your file tree. sourcesContent is the file contents — the actual TypeScript. Comments included. Developer notes included. Variable names you renamed at the last minute included.

The bundler emits this so your DevTools can show readable source when you hit a breakpoint. In production, that convenience hands every visitor your codebase.

The attack, end-to-end

Three minutes from "I want to see this app's source" to having it on disk. The pattern:

# 1. View-source on the landing. Find the bundle reference.
curl -s https://target-app.com/ | grep -oE 'src="[^"]*\.js"' | head -3
# <script src="/assets/index-h4Sd9.js">

# 2. Fetch the bundle, find the sourcemap reference.
curl -s https://target-app.com/assets/index-h4Sd9.js | tail -c 200
# ...//# sourceMappingURL=index-h4Sd9.js.map

# 3. Fetch the sourcemap.
curl -s https://target-app.com/assets/index-h4Sd9.js.map > bundle.js.map
ls -la bundle.js.map
# 1.4 MB — every file in src/ is in there.

# 4. Decode to a directory tree.
npx source-map-cli decode bundle.js.map -o reconstructed/
ls reconstructed/src/
# components/ lib/ utils/ pages/ ...

# 5. Grep for what matters.
grep -r "sk_live\|service_role\|whsec_\|sk-ant-" reconstructed/
grep -rn "TODO\|FIXME\|HACK\|XXX" reconstructed/
grep -rn "// internal\|// don't ship\|// rotate" reconstructed/

What you find depends on what's in the source. Common categories:

The volume of useful information per minute spent is high enough that bug-bounty hunters list "check for sourcemaps" as the second thing they do after fingerprinting the stack.

The per-bundler fix

Sourcemap emission is controlled in your bundler config. Set the right value, redeploy, verify from outside.

Vite

// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: false,                  // never emit
    // OR
    sourcemap: 'hidden',               // emit but no //# sourceMappingURL= comment in bundle
  },
});

'hidden' mode is what you want if you upload the maps to Sentry / Bugsnag — the maps exist but the bundle doesn't tell anyone where they are. The error-monitoring tool gets the maps via its own upload step.

Next.js

// next.config.js
module.exports = {
  productionBrowserSourceMaps: false,  // default since Next.js 12; verify
};

For older Next.js apps that explicitly enabled them: this is the toggle that turns them off. For Sentry users on Next.js: install @sentry/nextjs — its build plugin handles "emit-maps-but-don't-publish" for you.

webpack

module.exports = {
  mode: 'production',
  devtool: false,                      // never emit
  // OR
  devtool: 'hidden-source-map',        // emit but no comment in bundle
};

Specifically not: 'source-map', 'eval-source-map', 'inline-source-map'. The first emits a public-discoverable map; the second uses eval() (which also requires 'unsafe-eval' in your CSP — see the CSP article); the third inlines the entire map into the bundle (no separate file but the source content is still public).

esbuild / tsup

{
  "sourcemap": false,                  // or "external" or "linked"
}

"external" emits a separate .map file but adds no //# sourceMappingURL= comment to the bundle. Equivalent to webpack's 'hidden-source-map'.

Rollup

export default {
  output: {
    sourcemap: false,
    // OR
    sourcemap: 'hidden',
  },
};

SvelteKit / Astro / Remix / Nuxt

All of these delegate to Vite or esbuild for production builds. Use the appropriate underlying flag through the framework's adapter or vite config.

Verify from outside

After deploying with maps off, verify externally — your bundler config and what actually got deployed can disagree (caches, CDNs, build artefacts from old runs):

# Find a bundled JS file via view-source.
curl -s https://your-app.com/ | grep -oE 'src="[^"]*\.js"' | head -3

# For each, probe the .map URL.
curl -I https://your-app.com/assets/index-XXX.js.map
# 404 — good
# 200 — sourcemap still public; redeploy didn't take, or your CDN is caching

# Also check the bundle itself for sourceMappingURL comments.
curl -s https://your-app.com/assets/index-XXX.js | grep -o 'sourceMappingURL=[^[:space:]]*'
# Empty — good
# Returns a path — your bundle still points at a map (the map might 404 but search engines still try to fetch it; remove the comment)

Run a vibecheck scan after deploy and look at the Exposed paths section. Sourcemap findings link to /fix/exposed_sourcemap for the per-bundler reference.

If maps already shipped: assume reconstruction

If you've been deploying maps and just realized it: assume an attacker has the source. Bug-bounty hunters and supply-chain attackers both run sourcemap dumps continuously against deployed apps; the window between "first deploy with maps" and "someone who didn't have your source has it" is hours, not weeks.

  1. Stop emitting maps now. Per the configs above. Redeploy. Verify externally.
  2. Decode your own historical maps. npx source-map-cli decode bundle.js.map -o reconstructed/. Look at what would have been visible. Specifically search for: secrets (grep -rE "sk_live_|whsec_|sk-ant-|sk-|service_role|eyJ" reconstructed/), internal hostnames, sensitive comments, dead code that references rotated credentials.
  3. Rotate anything sensitive that was visible. Same response shape as the leak posts: service_role, Stripe, OpenAI / Anthropic, webhook secrets all apply.
  4. Audit your build pipeline. Add a CI check that fails the build if a .map file ends up in the deploy output: find dist/ -name "*.map" -print -exec false {} +. Catches the regression before it ships.

Three legitimate reasons to ship maps anyway

Sometimes you do want maps in production. The pattern is the same: hidden mode, plus a separate authenticated channel that delivers the map to whoever actually needs it.

  1. Error monitoring. Sentry, Bugsnag, Honeybadger, Rollbar — every error-tracking tool needs source maps to deminify stack traces. Use their CLI / build plugin to upload the maps at deploy time. The maps live on their infrastructure, accessed via API key, not on your public URL.
  2. Customer-debugging tools. A B2B product where support engineers occasionally need to see customer-side stack traces in original form. Build a customer-portal page that fetches the maps via authenticated server-side request and serves them only to logged-in support staff. Don't put them at /assets/*.map.
  3. Open-source apps. If your repo is public anyway, the calculus changes — the source is on GitHub, the sourcemap is just a cached copy. The "leak" doesn't add information. Ship maps if you want; they're convenient for users debugging your app.

For everything else: don't ship them.

Inspect your app for exposed sourcemaps