UnveilTech

UnveilScan Blog

← All articles

Try UnveilScan free

Source maps in production: why your minified app isn't actually minified

Posted 2026-04-29 · 5 min read · WEBbuild

You ship bundle.abc123.min.js to production — minified, transpiled, mangled variable names. You think the source is hidden. Then someone opens your site in Chrome DevTools, hits the Sources panel, and your unminified code with original variable names and inline comments is right there, fully readable.

That's a source map. Webpack puts //# sourceMappingURL=bundle.abc123.min.js.map at the bottom of your bundle. The browser fetches the .map file (which IS your unminified source) and reconstructs the original tree. Default behaviour for nearly every modern bundler.

Why this matters

Source maps are great in development (better stack traces, better debugging) and during error monitoring (Sentry, Datadog can de-minify stack traces). They are terrible in production-served:

How to detect

From your terminal:

$ curl -s https://example.com/ | grep -oE 'src=[^ ]*\.js' | head -3
src="/_app/immutable/start.7jP7wo7n.js"

$ curl -s https://example.com/_app/immutable/start.7jP7wo7n.js | tail -1
//# sourceMappingURL=start.7jP7wo7n.js.map

$ curl -sI https://example.com/_app/immutable/start.7jP7wo7n.js.map
HTTP/2 200    <- problem
content-type: application/json
content-length: 1547892

A 200 on the .map URL = your source is exposed. Most production sites we scan have at least one. UnveilScan's source_maps_exposed checker (Extended profile) automates this for up to 5 same-origin scripts.

How to stop

Webpack

// webpack.config.js
module.exports = {
  mode: 'production',
  devtool: false,  // never emit source maps for prod build
  // ...
};

If you need maps for monitoring (Sentry), build with devtool: 'hidden-source-map' — emits the .map file but doesn't include the sourceMappingURL reference at the bottom of bundles. You upload the .map to Sentry separately, then delete it from the prod artifact.

Vite / Rollup

// vite.config.js
export default {
  build: {
    sourcemap: false,
    // OR for hidden Sentry-style:
    // sourcemap: 'hidden'
  }
};

Next.js

// next.config.js
module.exports = {
  productionBrowserSourceMaps: false  // default
};

Next.js default is false. If you set it to true for Sentry, use the @sentry/nextjs SDK which uploads + strips automatically.

SvelteKit

// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
  kit: {
    adapter: adapter()
    // sourcemap controlled by Vite — see Vite section above
  }
};

Server-side fallback

If you can't easily change the build, block at the server level:

# nginx
location ~ \.map$ { return 404; }

# Apache
<FilesMatch "\.map$">
    Require all denied
</FilesMatch>

The Sentry use case

Sentry needs source maps to de-minify stack traces. The right pattern:

  1. Build with devtool: 'hidden-source-map' (or equivalent).
  2. Use the Sentry CLI / plugin to upload the .map files to Sentry's servers during deploy.
  3. Delete the .map files from your production artifact before serving.

Sentry's CLI handles steps 2 and 3 in one command. Document it in your CI pipeline.

How UnveilScan checks this

Our source_maps_exposed checker parses HTML for <script src>, picks up to 5 same-origin .js files, requests .map for each. We require the response to start with {"version":3 or contain "mappings" to avoid false positives from catch-all SPA fallbacks. Confirmed leaks emit a LOW finding with the URL of each exposed map.

Find your exposed source maps

Extended scan probes up to 5 .js files for accompanying .map exposure.

See pricing