Skip to content
KeyDrift
Scan for free
All posts

Source maps: the file you forgot you published

A source map reconstructs your original code from the minified bundle, comments and all. If it is served in production, so is everything the minifier removed.

KeyDrift4 min read
bundlesbuilds

What a source map contains

Minification does not obscure your code. It compresses it, and it does so lossily — variable names go, whitespace goes, comments go. What is left is hard for a person to read and completely legible to a machine.

A source map is the instruction set for undoing that. It maps every position in the minified output back to a position in the original file, and — this is the part people forget — it usually embeds the original sources themselves in a sourcesContent field. Not a reference to them. The actual text.

So a served .map file can contain your original module structure, your variable and function names, your TODO comments, the commented-out block somebody left in, internal endpoint paths that never appear in the shipped code, and the shape of code paths behind feature flags that no user has ever triggered.

None of that is a credential by itself. It is reconnaissance, and it turns "what does this application do" from a research project into a download.

How to tell whether you serve them

The sourceMappingURL comment

Bundlers append a comment to the end of each chunk pointing at its map.

BASE=https://example-app.test
curl -s "$BASE/_next/static/chunks/main-EXAMPLE.js" | tail -c 200

If the last line reads //# sourceMappingURL=main-EXAMPLE.js.map, a map is being advertised. That alone does not mean it is reachable — the comment can survive while the file is not deployed.

Fetching the map directly

The only test that settles it.

curl -sI "$BASE/_next/static/chunks/main-EXAMPLE.js.map" | head -1

A 404 is what you want. A 200 means anybody can fetch it. To see what they would get:

curl -s "$BASE/_next/static/chunks/main-EXAMPLE.js.map" \
  | head -c 400

If the response contains a sourcesContent array, your original source is being served alongside the minified version.

Checking every chunk, not one

Maps are generated per chunk, and configuration can vary — it is entirely possible to serve maps for your application code and not your vendor bundle, or the reverse.

curl -s "$BASE" | grep -oE '/_next/static/[^"]+\.js' | sort -u \
  | while read -r c; do
      printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' "$BASE$c.map")" "$c.map"
    done

Turning them off, or restricting them

The honest position is that source maps are genuinely useful — readable stack traces in production error tracking are worth a lot, and teams that disable maps entirely often end up debugging minified traces by hand. The goal is not to destroy them. It is to stop serving them publicly.

The usual arrangement is to generate maps, upload them to your error-tracking service during the build, and then delete them from the deployed output. Your traces stay readable; the public gets the minified file only.

Next.js

Production browser source maps are off by default. If somebody turned them on, it is a single flag:

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

Worth confirming rather than assuming — it is the kind of setting enabled once during a debugging session and never reverted.

Vite

Vite defaults to no maps in production builds. build.sourcemap: true emits and serves them; 'hidden' generates the map and omits the sourceMappingURL comment, which is the right setting when you are uploading maps to an error tracker.

// vite.config.js
export default { build: { sourcemap: 'hidden' } };

Note that 'hidden' still writes the file into your output directory. If you deploy that directory wholesale, the map is still fetchable by anyone who guesses the filename — which is trivial, because it is the chunk name with .map appended. Deleting maps after upload is the step that actually closes it.

Minification is not obfuscation, and your source map is the key.

Common questions

Is serving a source map a vulnerability?

Not on its own, and calling it one overstates it. It is an information disclosure that lowers the cost of every other attack — and occasionally it is worse than that, because comments and dead code have a habit of containing things the shipped code does not, including endpoint paths, internal identifiers and the occasional credential in a commented-out block.

Does the map contain my server code?

It should not. Maps are emitted for the bundles they describe, so a browser bundle's map covers browser code. The exception is a build misconfiguration that pulls server modules into a client chunk — in which case the map is not your problem, the chunk is, and the map is just how you found out.

If I remove the map now, is it gone?

From the next deploy, yes. Anyone who fetched it already has it, and caches and archives may continue serving the old file for some time. This is the same rule that applies to any published artifact: removal changes the future and not the past. Rotate anything a map exposed rather than assuming nobody read it.

Run a free scan

KeyDrift reads the JavaScript your app actually serves and finds the Supabase, Stripe, OpenAI and AWS keys that should never have left your server. Run a free scan.

KeyDrift is a Veristria product. More about KeyDrift.