DAST scanner · light requests

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.

DASTHighLowInformation disclosureGET only

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.

Candidates · max 40 1 source_maps_found maps the page already announced (//# sourceMappingURL= comments) 2 first-party JS assets .js .mjs .cjs only, vendor bundles skipped /assets/app.3f9a.js?v=2 → /assets/app.3f9a.js.map query and fragment stripped first duplicates dropped, first 40 kept GET each candidate GET …/app.3f9a.js.map 5 requests at a time 0.2 s delay between them redirects followed auth headers attached if present failed request: skipped stops when the time budget ends Confirm, then grade status == 200 any other status: dropped "sources": or "mappings": in the body, else dropped HIGH · first-party map host matches the page, confidence 0.95 LOW · third-party map another host, lower risk

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.

Built from the code, not a markdown doc

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.

request and response verdict GET https://app.example/assets/app.js.map404 Not Found quiet · status is not 200no body is inspected GET https://spa.example/assets/app.js.map200 <!doctype html><div id="root">… quiet · SPA catch-all page200, but neither marker is in the body GET https://app.example/assets/app.js.map200 {"sources":["src/App.tsx"],"mappings":"AAAA…"} HIGH · First-party source map exposedmarkers present, host matches the page GET https://cdn.vendor.net/lib.js.map200 {"sources":["lib/index.js"],"mappings":"…"} LOW · Third-party source map exposedreached via a sourceMappingURL hint

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.

Why it matters

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

Reference: what the code does

ItemValueWhere it comes from
Candidate sourcesthe ingestion list of source maps, then <asset>.map for each first-party JS asset_collect_candidates
Derived nameasset path must end in .js, .mjs or .cjs; query and fragment stripped; inline-script assets skipped_derive_map_url, _JS_SUFFIXES
Caps40 candidates; 5 concurrent requests; 0.2 s between requests; stops at the time budget_MAX_CANDIDATES, _MAX_CONCURRENT, _REQUEST_DELAY_SECONDS, TimeBudget
Confirmationstatus 200 and the body contains "sources": or "mappings":_SOURCE_MAP_MARKERS, _verify_map
Severitysame host as the page (leading www. ignored) → HIGH; otherwise LOW_build_finding, is_first_party_url
Findingcategory SOURCE_MAP_LEAK, source DAST_URL, confidence 0.95, method GET, endpoint = the map URL_build_finding
Authattaches the engagement's auth headers when present; works without themAuthAwareScanner
Scan depthruns at both quick and deepfactory.py DAST list
Things it does not do

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.