Source Maps
A .map file turns minified JavaScript back into the original source: file paths, comments, and anything a developer left in the code. isitsecure collects map URLs the page already announces, guesses <asset>.map for every first-party script, fetches each one, and reports it only if the body really is a source map. Single-page apps answer 200 to almost any path, so a bare 200 never counts.
Find, fetch, confirm
The scanner has three jobs: build a list of candidate URLs, request each one, and decide from the response body whether it is a genuine source map. Only then does it pick a severity.
1The first source of candidates is what URL ingestion already saw, mainly sourceMappingURL comments inside the page's scripts. Those URLs go on the list as they are.
2The second source is a guess. For each first-party script ending in .js, .mjs or .cjs, the scanner strips any query string and appends .map. Inline scripts are skipped. The list is deduplicated and capped at 40.
3Each candidate is requested with a plain GET, five at a time with a short delay, using the engagement's auth headers when there are any. The scanner stops early if its time budget runs out.
4A candidate survives only if the status is 200 and the body contains "sources": or "mappings":. Everything else is dropped without a finding.
5Grading looks at the map's host. Same host as the page → HIGH, because it exposes your own source. A map on another host → LOW, because it only exposes a vendor library.
This scanner has no page under docs/scanners/. Everything here is taken from isitsecure/engine/scanners/source_map_scanner.py and the README scanner-table row ("Publicly exposed .map files leaking original source (verified, not just present)"). The cap of 40, the concurrency of 5, the 0.2 s delay and the two body markers are read directly from that file.
Why a 200 is not enough
Many single-page apps route every unknown path to index.html and return it with status 200. If the scanner trusted the status code, every guessed .map URL on such a site would look exposed. The body check is what separates a leaked map from a catch-all page.
1Four guessed or announced map URLs, four different replies. The scanner has to tell the real leak from the three lookalikes using only the response.
2A 404 is the healthy case. Anything other than 200 is dropped before the body is read.
3A single-page app returns its shell with status 200 for any path. The body is HTML and contains neither "sources": nor "mappings":, so no finding. This is the guard the code comments call out.
4A real Source Map v3 file carries both keys. On the same host as the page it is reported as First-party JavaScript source map exposed, HIGH.
5The same body on another host is a vendor library's map. It is still reported, as LOW. Third-party maps can only be reached through the URLs ingestion already found, because guessing is limited to first-party scripts.
The finding text explains it: source maps "reconstruct the original, un-minified source code, exposing internal file paths, comments, and any secrets embedded at build time". Minification is the only thing between an attacker and a readable copy of your client logic, hidden endpoints and API shapes. That is why a first-party map is graded HIGH while a vendor map, which is public code anyway, is LOW.
Vulnerable vs fixed build config
The finding's advice is to disable source map generation in production or restrict access to the files. The tabs show where that switch lives in three common toolchains. These are illustrative; use the option your own build tool provides.
// vite.config.js
build: { sourcemap: true } // app.js.map deployed next to app.js
// webpack.config.js
devtool: 'source-map' // and //# sourceMappingURL= left in app.js
// next.config.js
productionBrowserSourceMaps: true// vite.config.js
build: { sourcemap: false }
// webpack.config.js
devtool: false
// next.config.js
productionBrowserSourceMaps: false
// Need maps for error tracking? Upload them to the tracker at build time
// and delete them from the public output, or serve them only behind auth.Reference: what the code does
| Item | Value | Where it comes from |
|---|---|---|
| Candidate sources | the ingestion list of source maps, then <asset>.map for each first-party JS asset | _collect_candidates |
| Derived name | asset path must end in .js, .mjs or .cjs; query and fragment stripped; inline-script assets skipped | _derive_map_url, _JS_SUFFIXES |
| Caps | 40 candidates; 5 concurrent requests; 0.2 s between requests; stops at the time budget | _MAX_CANDIDATES, _MAX_CONCURRENT, _REQUEST_DELAY_SECONDS, TimeBudget |
| Confirmation | status 200 and the body contains "sources": or "mappings": | _SOURCE_MAP_MARKERS, _verify_map |
| Severity | same host as the page (leading www. ignored) → HIGH; otherwise LOW | _build_finding, is_first_party_url |
| Finding | category SOURCE_MAP_LEAK, source DAST_URL, confidence 0.95, method GET, endpoint = the map URL | _build_finding |
| Auth | attaches the engagement's auth headers when present; works without them | AuthAwareScanner |
| Scan depth | runs at both quick and deep | factory.py DAST list |
It never reads the map's contents beyond the two markers, so it does not look for secrets inside sourcesContent. It guesses <asset>.map only; a bundler that names maps differently is found only when ingestion saw a sourceMappingURL. Vendor and framework bundles are filtered out of the guessing step. A different subdomain of your own site counts as third-party and gets LOW.