Skip to content
Book a test

home / research

// writeup · recon · javascript · bug-bounty · methodology

JavaScript Is a Recon Goldmine

Nov 4, 2026 · @sdx · 10 min read

Every modern web app sends its front-end code to every visitor. That code has to know how to call the API, so it carries a map of the backend: routes, parameters, roles, feature flags, and sometimes keys that were never meant to leave the build server. Reading it carefully is one of the highest-return habits in bug hunting.

This post covers how we collect and read JavaScript at SecureDX on pentests and bug bounty programs. Every example uses example.com. Only test targets you are authorized to test.

  1. 01Collectkatana · gau · chunks
  2. 02Source mapsrebuild original code
  3. 03Endpointsroutes · params · hosts
  4. 04Secretstriage before reporting
  5. 05Logicroles · flags · validation
  6. 06Watchdiff routes every build

Why bundles leak so much

A single-page app built with React, Vue or Angular is compiled into a few large JavaScript files. To work, those files must contain everything the app might ever do in the browser, for every type of user. The server decides who may do what; the bundle just knows how to ask.

What a typical production bundle contains:

What Example Why it matters
API routes /api/v2/admin/users/{id}/role Endpoints the UI never shows to normal users
Base URLs https://internal-api.example.com Hosts that never appear in DNS searches
Parameter names includeDeleted, asUser, debug Inputs to try that the UI never sends
Role and permission names isAdmin, canExport, role === "support" How the app thinks about access
Feature flags enableNewBilling, beta_reports Unfinished features, often with weaker checks
Third-party keys Analytics IDs, map keys, payment publishable keys Usually public by design, occasionally not
Comments and strings // TODO: add auth check, error messages Developer intent, in their own words

Minification shortens variable names, but it can’t shorten URLs, parameter names or strings the server expects. Those survive intact, and they’re what you’re after.

Step 1: Collect every JavaScript file

The files your browser loads on the homepage are only part of the app. Collect from three places: what a crawler finds today, what archives saw in the past, and the lazy-loaded chunks that only load when a user opens a certain screen.

# JS referenced by live pages (-jc also parses JS files for more links)
katana -u https://app.example.com -jc -d 3 -rl 20 -silent | grep -iE '\.js(\?|$)' > js_crawl.txt

# JS seen in archives, including old versions
gau app.example.com | grep -iE '\.js(\?|$)' > js_history.txt

# Merge, keep the ones that still respond
cat js_*.txt | sort -u | httpx -silent -mc 200 > js_live.txt

# Download, named by content hash so duplicates collapse
mkdir -p js && while read -r u; do
  f=$(curl -s "$u" | tee /tmp/body.js | sha1sum | cut -c1-12)
  mv /tmp/body.js "js/$f.js"; echo "$f $u" >> js_index.txt
done < js_live.txt

Lazy-loaded chunks. Webpack and Vite split apps into chunks loaded on demand, so a crawler that never opens the admin screen never sees the admin chunk. The main bundle usually holds a map of chunk IDs to file names. Search it for the chunk loader and for file name patterns:

grep -oE '"static/js/[a-zA-Z0-9_.-]+\.chunk\.js"' js/*.js | sort -u
grep -oE 'assets/[a-zA-Z0-9_-]+\.js' js/*.js | sort -u

Build the full URLs from those names and add them to your download list.

Archived versions are worth keeping separately. An old bundle can reference endpoints that were removed from the UI but are still running on the server.

Step 2: Look for source maps

A source map links minified code back to the original source files, so developers can debug production errors. When one is published, you get the original code: real variable names, the folder structure, and comments.

Find them in two ways:

# The bundle names its map in a comment on the last line
tail -c 300 js/*.js | grep -o 'sourceMappingURL=[^ ]*'

# Or try the .map URL directly for every bundle
sed 's/$/.map/' js_live.txt | httpx -silent -mc 200 -o maps_live.txt

Then rebuild the source tree:

sourcemapper -url https://app.example.com/static/js/main.4f2a.js.map -output src/

What to read first in the rebuilt source:

  • The folder structure. Names like src/admin/, src/internal/ or src/support-tools/ show whole areas of the app.
  • API client files such as api.ts or services/. They list every call in one place, with parameter types.
  • Route definitions. The router config shows every page and which role guard, if any, protects it.
  • Comments. TODO, FIXME, HACK and temporary mark the places developers knew were unfinished.

An exposed source map is usually a low-severity finding on its own. Its real value is the code it gives you to read.

Step 3: Pull out endpoints and parameters

Regex finds strings that look like paths. A JavaScript-aware parser does better: it understands string concatenation and template literals, so it can report /api/users/${id}/export as one route with a variable in it.

# Routes and URLs, parsed from the code structure
for f in js/*.js; do jsluice urls "$f"; done | jq -r '.url' | sort -u > js_routes.txt

# Quick regex pass for API-looking paths jsluice may miss
grep -rhoE '/(api|v[0-9]+|graphql|internal|admin)/[A-Za-z0-9_/{}$.-]*' js/ | sort -u >> js_routes.txt

# Base URLs: other hosts the app talks to
grep -rhoE 'https?://[a-zA-Z0-9.-]+\.example\.com' js/ | sort -u > js_hosts.txt

Turn the raw list into a route table you can work through:

Route Method Parameters Seen in UI? Notes
/api/v2/orders/{id} GET id Yes Test with a second account’s ID
/api/v2/admin/users/{id}/role PUT id, role No Admin-only route, test with a normal session
/api/v1/export POST format, includeDeleted No Old version, unused flag

The “Seen in UI?” column is the one to sort by. Routes the interface never calls are the least tested, because nobody clicking through the app ever reaches them.

GraphQL apps keep their queries and mutations as strings in the bundle. Search for query and mutation to list every operation the app knows, with field names, even when the server has introspection turned off.

grep -rhoE '(query|mutation) [A-Za-z0-9_]+' js/ | sort -u

Step 4: Triage secrets carefully

Secret scanners match patterns, so most hits in front-end code are keys that are meant to be public. Reporting those wastes the triager’s time and costs you reputation. Know which is which before you write anything up.

# Candidate secrets, parsed with context
for f in js/*.js; do jsluice secrets "$f"; done | jq -c '{kind, data}' | sort -u > js_secrets.txt
Key type Looks like Usually a finding? What decides it
Firebase web config apiKey: "AIza...", projectId No Public by design; check whether the database and storage rules allow anonymous access instead
Google Maps key AIza... Rarely Only if it has no referrer or API restrictions, and the program accepts that class
Stripe publishable key pk_live_... No Meant for the browser
Stripe secret key sk_live_... Yes, critical Never belongs in front-end code
AWS access key AKIA... Yes, if valid A key ID with its secret in the same file is serious
Slack or Discord webhook hooks.slack.com/services/... Often Anyone can post into the company’s channels
Private API tokens Bearer ..., long random strings near an auth header Often Depends on what the token can access
Analytics and tag IDs G-..., GTM-... No Public, but useful for finding sister sites

When a key looks real, prove it with the least invasive request possible: one read-only call that shows whose account it belongs to, such as an identity check. Never read data, send messages or change anything with it. Stop, record what you saw, and report it quickly so the company can rotate it.

Step 5: Read the logic, not just the strings

Endpoints tell you where to look. The code around them tells you what the developers assumed, and bugs live in wrong assumptions. Open the bundle in a formatter (your browser’s DevTools “pretty print” works) and search for these:

  • Role checks in the browser. Code like if (user.role === "admin") showDeleteButton() hides a button, but the server must also refuse the request. Call the route the button would call, with a normal user’s session. If the server accepts it, the only protection was the hidden button.
  • Feature flags. Names like enableBulkExport or beta_ show features that are built but switched off. A flag that only hides the UI often leaves the API live.
  • Debug and internal switches. Parameters such as debug=true, asUser, impersonate or mock sometimes still work in production.
  • Client-side validation. Limits enforced only in JavaScript (file types, price fields, quantity caps) may not be enforced again on the server.
  • IDs the client sends. If the app sends userId or accountId in a request body instead of relying on the session, test whether the server checks it belongs to you.

Search terms that pay off in formatted bundles: isAdmin, role, permission, feature, flag, debug, internal, TODO, deprecated.

Test each idea with two accounts you control. Proving that one of your accounts can read or change the other’s data is enough to show impact, without touching any real user’s information.

Step 6: Watch bundles change

A changed bundle usually means a new release, and new releases bring new routes and features that nobody has tested yet. Track the route list, not the raw files: bundle names change on every build even when the code barely does.

#!/usr/bin/env bash
# js-watch.sh: alert on routes that appear in a new build
cd ~/recon/app.example.com

# Re-collect and re-extract (steps 1 and 3)
./collect_js.sh && ./extract_routes.sh   # writes js_routes.txt

# Keep only routes never seen before, and remember them
anew routes_seen.txt < js_routes.txt > new_routes.txt
[ -s new_routes.txt ] && notify -silent -bulk -data new_routes.txt

Run it daily. When a new route appears, read the chunk it came from, add it to your route table, and test it first. You’ll often be looking at a feature hours after it shipped, before it has been tested by anyone else.

For developers: what not to ship

Everything above works because front-end code is public. You can’t hide it, so make sure nothing in it matters if read.

  • Turn off production source maps, or upload them only to your error-tracking service.
  • Keep secret keys on the server. Only keys designed for browsers (publishable, referrer-restricted) belong in front-end code. Add secret scanning to CI so a sk_live_ key fails the build.
  • Enforce every permission on the server. Hiding a button is a convenience, not access control. Each API route checks the session’s role and that the requested object belongs to that user.
  • Split admin code into a separate bundle, served only to logged-in admins.
  • Remove dead routes and flags from both the front end and the API when a feature is retired.
  • Never trust IDs sent by the client. Take the user and account from the session.
  • Strip comments in production builds; most bundlers do this by default, so check yours does.

Next: Part 2: Advanced Techniques covers route manifests, webpack chunk recovery, AST parsing and per-role traffic recording.


SecureDX tests web apps manual-first, starting with exactly this kind of JavaScript review. If you want your app checked, get in touch at securedx.in/contact. Hunters who work this way can request an SDX Society invite at securedx.in/society.

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Keep it on topic and don't post secrets or other people's data.