SOURCE MAPS AND .GIT IN PRODUCTION
Source maps reveal your unminified code. .git directories reveal your full history including secrets you thought were rotated. AI-codegen stacks ship both to production by default. Most people never notice.
The scenario referenced below runs on gapbench.vibe-eval.com — a public security benchmark we operate.
The leak nobody notices
Your production site ships JavaScript. The JavaScript is minified, bundled, fingerprinted with a content hash. Anyone who opens DevTools sees main.a1b2c3.js, an unreadable wall of single-letter variables. That’s the intended result.
Next to that file, by default, your build output also includes main.a1b2c3.js.map. If you hit it directly, you get a JSON file that maps every minified position back to your original source code, with the original variable names, the comments, the file paths, even some of the directory structure. Open the .map in DevTools and the unreadable bundle becomes your repo.
The bug isn’t that source maps exist — they’re useful for debugging your own code. The bug is shipping them publicly. Anyone with five minutes and curl can fetch your map files and reconstruct what’s effectively your private repo.
The .git case is worse
.git in production is the same shape, deeper. Your git directory contains every commit you’ve ever made, every branch you’ve ever pushed, every file you’ve ever staged. Including all the things you removed because they were a mistake. The Stripe key you committed and rotated. The internal API endpoint you accidentally hardcoded and then sed-replaced. The 50MB CSV of test data with real PII someone pasted into a fixture file and removed in the next commit. Git remembers all of it.
If /.git/ is reachable on your production domain, anyone who runs git-dumper or its equivalents pulls the whole thing in seconds. Your private repo is now their private repo, including everything you removed but didn’t purge.
The two URLs that confirm the bug:
curl https://gapbench.vibe-eval.com/site/git-exposed/.git/config— returns the config file.curl https://gapbench.vibe-eval.com/site/git-exposed/.git/HEAD— returns the HEAD ref.
If either returns 200 on your site, you have it.
Why this AI-codegen pattern survives
Two reasons stack.
The Dockerfile pattern. A typical AI-suggested Dockerfile contains COPY . /app/. The .dockerignore file should list .git, *.map, node_modules, and so on. AI generators sometimes write the .dockerignore, sometimes don’t. When they don’t, everything in your repo, including .git, ends up in the image, which gets deployed.
The build-output pattern. Next.js, Vite, and similar tools have configuration for whether source maps are emitted in production builds. The defaults vary. Vite ships maps to production by default if you don’t override. Next.js does not, by default, but enables them with one config flag that the AI cheerfully suggests when you ask “how do I debug production issues.” That flag stays on, and you ship maps.
In both cases, the bug is invisible from the developer’s perspective. The build succeeds. The site loads. Nothing in the deploy log says “warning: shipping your repo to the public internet.”
A specific incident
Anonymized. A B2B product had been running for ~18 months when we ran our scan. The first finding back was /.git/HEAD returning ref: refs/heads/main. Their entire git history was reachable. We pulled it down with git-dumper to confirm, and the history contained:
- A Stripe live secret key the team had committed in 2024 and rotated in early 2025. Key was still in history.
- A migration script with a bcrypt hash of an admin password from a test database that turned out to be the same hash they used in prod.
- Internal URLs for an admin tool the team didn’t realize was reachable from the public internet.
- Comments referring to specific customer names in commit messages — embarrassing more than dangerous.
The deploy path that introduced the bug: a Dockerfile that used COPY . /app/, no .dockerignore for .git. The team had been deploying Vercel for the public-facing app and self-hosted Docker for a backend API; the API’s image included .git from the start. Nobody had checked.
Cleanup took a week. Rotate every credential that had ever been in history. Run git-filter-repo to scrub sensitive files (which only helped going forward — the leaked artifact was already public). Add .dockerignore and CI checks. Audit logs for any access to /.git/* paths during the 18-month window — and there had been some, mostly bots, possibly some real attackers, no way to be sure.
The lesson: .git exposure is silent and total. By the time you find it, anyone who wanted it has had it. The mitigation is “don’t ship the directory in the first place.”
How source maps reveal more than people realize
Source maps don’t introduce new secrets — anything in the bundle was already in the bundle, the map just makes it readable. But they do reveal the shape of your code in ways that matter for attack planning:
- File and folder structure of your repo. An attacker reading your map files knows your route layout, your component naming, your internal abstractions.
- Comments inside your code that survive minification. We’ve seen TODO comments referencing “the admin endpoint nobody knows about,” internal URLs, debug-only flags.
- Variable names that hint at security-relevant logic. “isAuthorized,” “bypassAuth,” “adminOverride” — once visible, they suggest where to probe.
- Imports that hint at unused-but-loaded code paths. Sometimes the bundle includes a feature flag-gated path that’s not active in production but is reachable; the map reveals it exists.
The fix is straightforward — don’t ship maps. The variation worth noting: most error trackers (Sentry, Bugsnag, Rollbar) need your source maps to symbolicate stack traces. The right pattern is to upload maps privately to the error tracker and not serve them publicly. Sentry has tooling for this; configure it correctly and remove the public maps.
Wrong fix vs right fix
// WRONG: thinking the env-only check is enough
// next.config.js
module.exports = {
productionBrowserSourceMaps: process.env.NODE_ENV !== 'production',
// What if NODE_ENV is unset on a misconfigured deploy?
// What if a later change overrides this?
}
// RIGHT: hard-pin to false for the public artifact
// next.config.js
module.exports = {
productionBrowserSourceMaps: false, // never in production bundle
// separate hidden source maps + Sentry upload via webpack plugin
}
# WRONG: copy everything, hope .dockerignore catches issues
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# RIGHT: copy only what's needed for build, then only what's needed for run
FROM node:20 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY src ./src
COPY tsconfig.json ./
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
CMD ["node", "dist/server.js"]
# .git never enters the runtime image. node_modules has no .git. No source maps.
Other artifacts in the same family
The detection net should cover more than just .git and source maps. Common siblings:
.svn/,.hg/— same shape as.git/for SVN and Mercurial..DS_Store— macOS folder metadata. Reveals filenames in the parent directory. Low impact alone, embarrassing when present.Thumbs.db— Windows equivalent. Same shape.*.bak,*.old,*.swp— editor backup files. Often contain unminified source.composer.json,composer.lock,package.json,package-lock.jsonexposed in unexpected paths — useful for attackers fingerprinting dependencies..env,.env.local,.env.production— these can leak. Less common with modern frameworks but happens.wp-config.php.bak(WordPress) — credential-laden config backup. Frequent finding on WP installs.
How we detect it
For source maps: we fetch the production HTML, parse out script src URLs, append .map to each, and request them. Any 200 response containing JSON-shaped source-map content is a finding.
For .git: we hit /.git/config, /.git/HEAD, /.git/index, /.git/refs/heads/main. Any 200 with the expected content shape is a finding.
For neither: we also check a handful of related artifacts — /.env, /.svn/, /.DS_Store, /package.json if it returns from a Next.js static path it shouldn’t, /.next/, build manifests with internal paths. The full list is in the vibe-code-scanner.
Fix
For source maps in production:
- Vite: set
build.sourcemap: falseinvite.config.ts. Or'hidden'if you want the maps generated for upload to an error tracker but not served at the public URL. - Next.js: don’t enable
productionBrowserSourceMaps. If you want them for Sentry, configure Sentry to upload them privately and remove them from the public artifact. - Webpack:
devtool: falsefor production, or'hidden-source-map'if you upload them to an error tracker.
For .git exposure:
- Add
.gitto your.dockerignore, your.vercelignore, and any other deploy-ignore file. - If your CI clones into the same directory as the deploy artifact, change the build to copy only the build output, not the project root.
- For Apache/Nginx in front of static deploys, add a hard rule:
location ~ /\.git { deny all; }. Belt-and-suspenders.
CWE / OWASP
- CWE-200 — Information Exposure
- CWE-538 — File and Directory Information Exposure
- OWASP Top 10 — A05:2021 Security Misconfiguration
Reproduce it yourself
- Next.js source maps shipped: https://gapbench.vibe-eval.com/site/nextjs-app/
- .git/ exposed: https://gapbench.vibe-eval.com/site/git-exposed/
- Config leak (related): https://gapbench.vibe-eval.com/site/config-leak/
- DevOps backup leak: https://gapbench.vibe-eval.com/site/devops-leak/
Related reading
COMMON QUESTIONS
SCAN YOUR DEPLOY FOR LEAKED ARTIFACTS
We hit your domain for source maps, .git directories, and other build artifacts that shouldn't be public.