Engineering Diaries · Security
§
SecurityBuild SystemsFrontend Infrastructure

Your Production Source Maps Are Leaking Your Entire Codebase to Anyone Who Knows Where to Look

Most frontend teams ship source maps to production without realising they're handing anyone a complete, readable copy of their application code. Here's what's actually inside them, how they get exploited, and what to do about it today.

Code on a dark terminal screen
01 · The Incident

The Audit That Made Me Question Every Project I'd Ever Shipped

Three years ago, I was doing a security review on a mid-sized SaaS product - one of those routine check-the-boxes exercises that engineering teams do before a compliance audit. I wasn't expecting to find anything interesting. The backend was reasonably locked down, secrets were in environment variables, the API had proper authentication. Standard stuff.

Then I opened the browser's developer tools, navigated to the Sources panel, and saw something that took a moment to process: the entire frontend codebase, fully readable, perfectly indented, with all the original variable names and comments intact. Not the minified production bundle - the actual source code. Component names, utility functions, the business logic that calculated pricing tiers, comments left by engineers explaining workarounds for specific edge cases.

We hadn't made a mistake in the traditional sense. No secrets were hardcoded. The application was functioning correctly. But anyone who opened DevTools on the production URL could read our codebase as easily as if they had access to the private GitHub repository.

The mechanism was source maps - and the configuration that had enabled this was the default setting recommended in half a dozen popular tutorials.

I've since done variants of this check on maybe thirty different frontend applications, ranging from small startups to products used by millions of people. The majority of them have the same problem. Some of the largest names in the industry have shipped production source maps at one point or another. This is not an edge case. It is the default outcome when teams don't consciously think about it.

"The source maps weren't a mistake. They were the default - which is precisely what makes this problem so widespread and so easy to overlook."

02 · The Contents

What Source Maps Actually Contain - It's Not Just Line Numbers

The mental model most engineers carry around for source maps is something like: "they help the browser show you the right line number when something errors." That's accurate, but it understates what's actually in those files by a significant margin.

A source map is a JSON file. Open one and you'll find several fields, but the one that matters for security is sourcesContent. This is an array that contains the complete, verbatim text of every original source file that was compiled into the bundle. Not references to the files. Not checksums. The actual content of each file, in full.

Source Map Structure (simplified)
{
  "version": 3,
  "file": "main.a3f9b2c1.js",
  "sources": [
    "webpack://myapp/src/components/PricingCalculator.tsx",
    "webpack://myapp/src/utils/discountEngine.ts",
    "webpack://myapp/src/api/internalEndpoints.ts",
    // ... every file in the bundle
  ],
  "sourcesContent": [
    "// PricingCalculator.tsx\nexport const calculatePrice = (plan, seats) => {\n  // Enterprise discount: if seats > 50, apply 23% reduction\n  // This logic mirrors what sales tells customers verbally\n  ...",
    "// Full content of discountEngine.ts ...",
    // ... full text of every source file
  ],
  "mappings": "AAAA,SAAS,OAAO...",
  "names": ["calculatePrice", "applyDiscount", "ENTERPRISE_THRESHOLD"]
}

The sources array lists the original file paths - often revealing your internal project structure and naming conventions. The names array contains original variable, function, and class names that minification stripped out. And sourcesContent delivers the complete text of each file.

When the browser loads a source map, it can reconstruct a perfectly readable version of your source tree in the Sources panel. It is, for all practical purposes, equivalent to handing someone access to your private repository - minus the git history.

The Size Tell

A quick way to confirm whether a production bundle is accompanied by a source map is to compare file sizes. A typical minified JavaScript bundle for a medium-sized React application might be 200-400 KB. Its corresponding source map is routinely 3-6 times larger - often 1-2 MB. If you're serving that alongside your application, you're serving a complete reproduction of your source code to every visitor.

Many teams notice the .map files in their build output and simply assume they're a necessary artefact of the build process. They're not - they're optional, and whether they're served publicly is entirely a configuration choice.

The File Paths Are Also Informative

Even if sourcesContent were somehow stripped, the sources array alone leaks meaningful information. Internal file paths reveal project structure, naming conventions, what libraries you're using, and sometimes the names of internal tools and services. Paths like webpack://myapp/src/integrations/salesforce/legacyAdapter.ts or webpack://myapp/src/features/aiRecommendations/experimentalEngine.ts tell a competitor quite a lot about your technical architecture.

03 · The Attack Surface

How Attackers Find and Extract Source Maps in Under Five Minutes

The discovery mechanism is not sophisticated. Minified JavaScript files reference their source map in a comment on the final line of the file:

Source Map Reference in Production Bundle
// main.a3f9b2c1.js (final line)
//# sourceMappingURL=main.a3f9b2c1.js.map

The browser reads this comment and automatically fetches the .map file from the same URL path. Which means an attacker only needs to:

  1. Open your production site and let it load normally.
  2. Open DevTools - Network tab - filter by "map" or look at the Sources panel.
  3. Download the .map file directly (the URL is right there in the network request).
  4. Use any source map consumer library to reconstruct the full source tree.

There are automated tools that do this in bulk. source-map-explorer is widely known for legitimate bundle analysis - it reads source maps to show you which code is taking up the most space. The same capability can be used to read your entire codebase. Tools like unwebpack and various open-source scripts go further, reconstructing a navigable file tree from the source map contents.

Five-Line Script to Extract Any Public Source Map
import { SourceMapConsumer } from 'source-map';
import fs from 'fs';

const rawMap = JSON.parse(fs.readFileSync('main.a3f9b2c1.js.map', 'utf8'));
const consumer = await new SourceMapConsumer(rawMap);

// List all original files and their full contents
consumer.sources.forEach(source => {
  const content = consumer.sourceContentFor(source);
  fs.writeFileSync(source.replace('webpack://', ''), content);
});

Run this against a downloaded source map and you get a fully reconstructed file tree on your local machine. The whole process takes less than five minutes. It requires no special access, no credentials, and leaves no traces in your application logs beyond an ordinary HTTP request for a .js.map file.

Automated Scanning at Scale Security researchers and red teamers routinely include source map discovery in automated reconnaissance pipelines. Tools like nuclei have templates specifically for detecting publicly accessible source maps. Bug bounty hunters actively look for them because the intelligence value is high. If your application has ever been targeted by a bounty hunter or a determined attacker, this check has very likely already been run against you.
04 · Framework Defaults

The Build Tool Defaults That Set Most Teams Up to Fail

The reason this problem is so widespread is not malice or carelessness - it's that the default configurations of popular build tools, combined with tutorial content that doesn't distinguish between development and production needs, have quietly normalised shipping source maps publicly.

Create React App - The Most Widespread Culprit

For several years, Create React App was the entry point into React for a majority of new projects. Its default production build included full source maps. The environment variable to disable them - GENERATE_SOURCEMAP=false - existed but wasn't mentioned prominently in documentation. An enormous number of production applications built with CRA during its peak years shipped source maps publicly. Many still do.

webpack - Dangerous by Tutorial

webpack's devtool option controls source map generation and has many possible values. The option most commonly found in tutorials and "production optimisation" guides is 'source-map' - which generates a full, external .map file that is publicly served. This is also the value webpack itself recommends for production in its own documentation, with the justification that it's best for quality debugging.

What the documentation doesn't highlight prominently enough is that 'source-map' means the .map file is served publicly alongside your bundle. The alternative 'hidden-source-map' generates the same file but removes the referencing comment from the bundle - meaning the browser won't automatically fetch it, but you can still upload it to your error tracking service.

webpack devtool Options - What They Actually Do
// PUBLICLY SERVES full source maps (do not use in production)
devtool: 'source-map'

// Generates .map files but does NOT reference them in bundles
// Upload to Sentry/Datadog manually; browser cannot find them
devtool: 'hidden-source-map'

// No source maps whatsoever - maximum obscurity
devtool: false

// Inline source maps embedded in the bundle itself (worst option -
// the map is embedded in the JS, making the file massive and fully public)
devtool: 'inline-source-map'

Vite and Next.js - Better Defaults, But Easy to Override

To their credit, both Vite and Next.js have better production defaults. Vite sets build.sourcemap to false in production builds. Next.js does not include browser source maps by default and requires an explicit opt-in via productionBrowserSourceMaps: true in next.config.js.

The problem is that engineers who are used to the webpack ecosystem often explicitly enable source maps in Vite or Next.js configs without realising the implications, copying configuration snippets from Stack Overflow answers or blog posts that were written in the context of webpack without this distinction in mind.

The Netlify and Vercel Edge Case

Some deployment platforms offer to upload source maps to Sentry or other error trackers as part of their deployment pipeline. This is genuinely useful and correctly implemented. But the mechanism sometimes involves generating the source maps first as build artefacts - and if those artefacts end up in the public directory rather than being consumed and discarded, they become publicly accessible.

Always verify after any deployment pipeline change that your .map files are not publicly accessible. The platform's intention may be correct; the configuration may still produce the wrong outcome.

05 · The Real Risk

The Business Logic Problem Nobody Mentions

Most discussion of source map exposure focuses on "attackers can see your code." That's true, but the phrasing undersells the actual risk in a way that causes teams to dismiss it. Let me be more specific about what's actually in that code that you don't want competitors and attackers to read.

Pricing and Commercial Logic

Frontend applications frequently contain pricing calculation logic, discount rules, and the thresholds at which different commercial terms apply. This logic often lives in the client because it powers UI features like real-time price previews or plan comparisons. In source form, with original variable names and comments written for internal developer clarity, this becomes a complete read on your commercial strategy. A competitor reading const ENTERPRISE_DISCOUNT_THRESHOLD = 50 and the associated discount percentage is reading your sales playbook.

API Endpoint Discovery

Even when API endpoints aren't secret per se - they're protected by authentication - knowing their exact paths, query parameter conventions, and the request/response shapes they expect dramatically reduces the effort required to exploit any authentication vulnerabilities that exist or emerge later. Source code typically contains every API endpoint the application uses, organised by function in a way that's far more useful than anything a network interceptor would reveal.

Feature Flag Logic and Unreleased Features

Modern applications routinely ship code for unreleased features controlled by feature flags. That code is in the bundle - and in the source map. An attacker who can read your source maps can see your product roadmap, understand how to force-enable unreleased features (sometimes simply by setting a localStorage value or a cookie), and identify endpoints that power those features before they're publicly documented.

Third-Party Integrations and Internal Tool Patterns

The pattern of how you integrate with third-party services - the specific API endpoints you hit, the payload structures you use, the way you handle errors - can be used to probe those integrations for weaknesses. Internal tool names, Jira project prefixes in TODO comments, Slack channel names in error reporting code - source maps reveal a surprising amount of your organisational infrastructure.

"Source map exposure is not just a code quality concern. It is a competitive intelligence problem, a security surface expansion, and occasionally a direct route to privilege escalation - all at once."

06 · The Audit

How to Audit Your Own Application in Ten Minutes

Before you change any configuration, establish whether you're actually affected. Here are three checks, in order of increasing thoroughness.

Check 1: The DevTools Sources Panel

Open your production application in Chrome or Firefox. Open DevTools. Click the Sources tab (Chrome) or Debugger tab (Firefox). If you can see a reconstructed file tree with your original component names, TypeScript files, and utility modules - you're serving source maps publicly. This takes thirty seconds.

Check 2: The Network Filter

In the Network tab, filter by ".map". Reload the page. If any .map files appear in the network requests - with successful 200 status codes - they're being served. Note the URLs.

Check 3: The Direct Fetch Test

Take the URL of one of your JavaScript bundles and append ".map" to it. Fetch it directly in a new browser tab. If you get a JSON response (source map content) rather than a 404, the file is publicly accessible.

Quick Audit Script - Check Any Domain
# Step 1: Find the JS bundle URL from the page source
BUNDLE_URL="https://yourapp.com/static/js/main.a3f9b2c1.js"

# Step 2: Check if a source map reference exists in the bundle
curl -s $BUNDLE_URL | tail -5 | grep sourceMappingURL

# Step 3: Attempt to fetch the source map directly
curl -o /dev/null -s -w "%{http_code}\n" "${BUNDLE_URL}.map"

# If the last command returns "200", your source maps are public
# If it returns "404", you're clean

If you manage multiple products, this audit is worth running across all of them. The outcome often varies between applications - a newer product built with Vite may be clean while an older one built with CRA or a custom webpack config is not.

~60%
Production React apps with public source maps (estimated from bounty reports)
5 min
Time to extract full source tree from a public map
3-6x
Larger than bundle - typical source map file size
0 logs
Traces left in your application when a map is downloaded
07 · The Fix Spectrum

From Vulnerable to Secure: The Source Map Fix Spectrum

There is a range of approaches here, from "completely exposed" to "maximum obscurity", with a useful middle ground that preserves debuggability without public exposure. Choose based on how sensitive your codebase is and what error tracking infrastructure you have.

Source Map Security Spectrum
Public Source Maps
Hidden Maps (no reference)
Error Tracker Only
No Maps at All

Option 1: No Source Maps (Maximum Obscurity)

The simplest fix. Disable source map generation entirely in your build. No .map files are produced; no source information is ever served. The downside is that stack traces in production error reports will show minified code - which is harder but not impossible to debug if your error tracker captures enough surrounding context.

This is the right choice for applications with highly sensitive business logic where even server-side access to source maps carries organisational risk, or for teams that don't yet have a proper error tracking setup and won't be uploading maps anywhere anyway.

Option 2: Hidden Source Maps Uploaded to Error Tracker (Recommended)

This is the configuration most production teams should be using. Source maps are generated as part of the build, but the referencing comment is stripped from the bundle so the browser never fetches them automatically. The .map files are uploaded to your error tracking service (Sentry, Datadog, Bugsnag, etc.) via their CLI or webpack plugin, then deleted from the build output before deployment.

This gives you full, readable stack traces in your error tracker - with original file names, line numbers, and code context - while ensuring that the maps are never publicly accessible. It requires a small amount of CI/CD configuration but is entirely standard practice.

Option 3: Server-Side Access Controls

If you need source maps in your deployed environment for some reason and can't or won't use an external error tracker, you can serve them under a URL that requires authentication, or restrict access at the CDN/server level by IP address (typically your office/VPN range). This is more operationally complex and has failure modes (a misconfiguration exposes everything) but is viable for teams with mature infrastructure.

08 · The Configuration

The Exact Configuration for webpack, Vite, and Next.js

Here is the production-ready configuration for each major build tool. Copy, adjust for your project, verify with the audit checks from Section 06.

webpack - The Right devtool Value

webpack.config.js - Production
module.exports = {
  mode: 'production',

  // Option A: No source maps at all
  devtool: false,

  // Option B: Generate maps for Sentry upload, but hide from browsers
  devtool: 'hidden-source-map',

  // Add the Sentry webpack plugin if using Option B
  plugins: [
    new SentryWebpackPlugin({
      authToken: process.env.SENTRY_AUTH_TOKEN,
      org: 'your-org',
      project: 'your-project',
      include: './dist',
      // Delete maps from dist after upload
      cleanArtifacts: true,
    }),
  ],
};

Vite - Explicit Production Config

vite.config.ts - Production
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    // Option A: No source maps (Vite default - explicit is better)
    sourcemap: false,

    // Option B: Generate hidden maps for error tracker upload
    // 'hidden' omits the sourceMappingURL comment (same as hidden-source-map in webpack)
    sourcemap: 'hidden',
  },
});

Next.js - The Config to Leave Alone

next.config.js - Keep productionBrowserSourceMaps Off
/** @type {import('next').NextConfig} */
const nextConfig = {
  // This is FALSE by default. Never set it to true in production.
  // productionBrowserSourceMaps: true, <-- DO NOT DO THIS

  // For Sentry integration, use @sentry/nextjs which handles
  // hidden source maps correctly via its own config
};

module.exports = nextConfig;

Create React App - The Environment Variable Fix

.env.production
# Disable source map generation in CRA production builds
GENERATE_SOURCEMAP=false
After Every Config Change - Re-audit Build your application with the new configuration, deploy to a staging environment, and run the audit checks from Section 06 before promoting to production. It is easy to have the right configuration in one webpack config file but a legacy one in another, or to have a CI/CD step that regenerates maps after the build step that was supposed to suppress them.
09 · The Nuance

When Source Maps Are Worth Keeping - and How to Do It Safely

I've been fairly forceful about the risk, so let me be equally clear about where source maps provide genuine value that's worth preserving - just not publicly.

Production Error Debugging Is Legitimately Hard Without Them

Minified production stack traces are painful. Variable names are single letters. Line numbers are meaningless. If you've ever tried to debug a production incident with nothing but a minified trace, you know how much time it wastes. Source maps, when properly uploaded to a service like Sentry or Datadog, completely solve this: you see the original file name, the original line, the original code context, and in some tools a full symbolicated stack trace. This is not a nice-to-have - it is a meaningful operational capability for any team running a product they care about debugging quickly.

The key phrase is "uploaded to a service." The maps should never be publicly served - but they absolutely should exist somewhere your error tracking can use them.

The Correct Architecture for Source Maps in Production

The pattern that gives you everything you want - debuggable errors, no public exposure - looks like this:

  1. Build with hidden-source-map (webpack) or sourcemap: 'hidden' (Vite). Maps are generated as files but not referenced from bundles.
  2. In your CI/CD pipeline, upload the .map files to your error tracker using its official CLI or plugin, scoped to the current release version.
  3. Delete the .map files from your build output before deploying to your CDN or server. They should never reach the public-facing file system.
  4. Verify: your CDN should return 404 for any .map URL. Your error tracker should correctly symbolicate stack traces.
One More Thing: Your CDN Cache

If you've historically served source maps and are removing them now, you need to explicitly purge them from your CDN cache. A CDN may continue serving a cached .map file long after you've removed it from your origin, depending on your cache-control headers and TTL settings. Issue a targeted cache purge for .map URL patterns after you deploy the fix.

This is also a good moment to audit your cache-control headers for .map files specifically. Even if they're temporarily accessible during a deployment window, a short TTL (or no-cache) limits the window during which someone can download them.

10 · Starting Today

The Practical Checklist for Right Now

Let me collapse everything into a checklist you can work through today, in roughly the right order of priority.

  1. Audit your production applications. Open DevTools Sources panel on every production URL you own. Run the curl check from Section 06. Know exactly which applications are affected before you touch any config.
  2. Check your CI/CD for map file artefacts. Look at what files get deployed to your CDN or server. If .map files appear in the deployment artefact list, they're being served. Fix the deployment step, not just the build config.
  3. Set up error tracker source map upload. If you don't have Sentry or equivalent configured, this is the moment to do it. The setup cost is a few hours; the operational benefit is permanent. Without this, removing source maps will make production debugging painful.
  4. Update build configs to hidden-source-map or false. Use the configurations from Section 08. Prioritise the applications with the most sensitive business logic.
  5. Purge CDN cache for .map URL patterns. After deploying, purge cached .map files from your CDN. Verify with the fetch test that they return 404.
  6. Add a CI check that fails if maps are publicly accessible. Automate the audit. After every production deployment, run a step that checks for accessible .map URLs and fails the pipeline if any are found. This prevents regression.
  7. Document this in your frontend security checklist. New projects get started all the time. Add source map configuration to whatever new-project checklist your team uses. The problem is most common with new applications that haven't had a security review yet.

"The fix takes an afternoon. The exposure - for many teams - has been running for years. That asymmetry is what makes this one of the highest-leverage security improvements a frontend team can make."

I want to end with the observation that this problem exists not because engineers are careless but because the ecosystem's defaults evolved with a primary focus on developer experience rather than production security. Source maps are genuinely useful tools. The mental model of "I configure this once for debugging and then forget about it" is understandable - it's how most build configuration works.

But source maps straddle the line between developer tooling and production artefact in a way that nothing else in the frontend stack quite does. That makes them easy to misconfigure and the consequences easy to underestimate. The right configuration is not difficult once you know what you're looking for. And now you do.

· · ·
Tooling Referenced in This Article

webpack hidden-source-map Vite sourcemap: hidden Sentry webpack plugin source-map-explorer @sentry/nextjs

All configurations described are for production builds. Development builds should always have full source maps enabled - the concern here is exclusively about what ships to users. The patterns described (hidden source maps + error tracker upload) are industry-standard practice at companies that have done this work, and the tooling for all of them is mature and well-documented.

The percentage figures cited regarding source map exposure rates are approximate, based on patterns observed across bug bounty reports and publicly documented security reviews. The configuration examples are accurate as of the library versions current in mid-2026 - always verify against the official documentation for your specific version.