Sourcemap leaks in vibe-coded apps: how attackers reconstruct your codebase from a .map file.
//# 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:
- Secrets compiled in. A developer hard-coded an API key during prototyping, the bundler picked it up, the bundle minified it (so it's not searchable in the production JS as plaintext), but the sourcemap restores the original literal. The secrets detector in vibecheck explicitly re-runs against decoded sourcemap content for this reason.
- Internal API URLs. Endpoints you don't link from anywhere indexable, but reference from frontend code.
https://api-internal.your-app.com/admin/...shows up. The path is now part of the attacker's reconnaissance map. - Authentication flow logic. The exact code paths your app uses for login, token refresh, role checks. Useful for finding logic bugs (the "this branch only runs for users with role=admin and we forgot to validate the role server-side" pattern).
- Comments and developer notes. "TODO: rate-limit this endpoint", "HACK: bypass billing for prelaunch users", "FIXME before launch", "rotate this on Monday."
- Dead code. Functions that aren't used anymore but weren't deleted. Sometimes they reference rotated credentials, decommissioned endpoints, deprecated payment flows. Even if they don't run in production, they document past architecture decisions.
- Recently-removed sensitive code. The PR that removed an admin shortcut last month — its replacement is in the current source, but the sourcemap of an older deploy (still cached on a CDN edge or archive.org) shows the old version.
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.
- Stop emitting maps now. Per the configs above. Redeploy. Verify externally.
- 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. - Rotate anything sensitive that was visible. Same response shape as the leak posts: service_role, Stripe, OpenAI / Anthropic, webhook secrets all apply.
- Audit your build pipeline. Add a CI check that fails the build if a
.mapfile 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.
- 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.
- 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. - 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