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.json exposed 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: false in vite.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: false for production, or 'hidden-source-map' if you upload them to an error tracker.

For .git exposure:

  • Add .git to 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

COMMON QUESTIONS

01
What's wrong with shipping source maps to production?
Source maps let anyone reconstruct your unminified, commented source from the production bundle. They reveal class names, function names, file structure, and inline comments. They don't usually reveal new secrets — anything in the bundle was already in the bundle — but they reduce reverse-engineering effort from days to minutes. For an attacker probing your client-side logic for bugs, source maps are an upgrade from a treasure map to a guided tour.
Q&A
02
What's wrong with .git in production?
If your build process copies your project root to the deploy artifact and your project root contains a .git directory, anyone can fetch /.git/config and walk back your commit history. Commit history typically contains secrets you rotated months ago, debug code with hardcoded URLs, removed-but-not-purged credentials. Tools like GitTools clone the directory in seconds. Your historical bugs are not historical from an attacker's perspective.
Q&A
03
Why do AI-codegen stacks ship these by default?
Source maps are on by default in many Vite, Next.js, and Webpack configs because developers want them in dev. The build pipeline doesn't always strip them for production unless explicitly configured. .git in production is sneakier — it usually arrives because a Dockerfile uses 'COPY . .' or because a Vercel project ignores .gitignore on the deploy artifact. The AI generator typically writes the COPY line and doesn't think about what's in '.'.
Q&A
04
How do I know if I'm leaking these?
Two URLs to test. https://your-site.com/.git/config — if you get back a Git config file, you're leaking the directory. https://your-site.com/static/js/main.abc123.js.map — if you get back a JSON-shaped source map, you're leaking maps. Most CDNs return 404 for both; if either returns 200, fix it before someone enumerates the rest.
Q&A
05
Where can I see this on a real URL?
https://gapbench.vibe-eval.com/site/nextjs-app/ ships source maps. https://gapbench.vibe-eval.com/site/git-exposed/ ships /.git/. Both are fetchable directly.
Q&A
06
What CWE does this map to?
CWE-200 (Information Exposure), CWE-538 (File and Directory Information Exposure). OWASP A05:2021 (Security Misconfiguration).
Q&A

SCAN YOUR DEPLOY FOR LEAKED ARTIFACTS

We hit your domain for source maps, .git directories, and other build artifacts that shouldn't be public.

RUN THE SCAN