JavaScript Recon: Mining a Web App's Frontend for Its Backend

The frontend JavaScript is a map of the backend: every API endpoint, hidden parameter, and sometimes a leaked key. How I collect a web app's JS, pull endpoints and secrets out of it, use source maps, and turn it into undocumented attack surface.

Modern web apps are mostly JavaScript. The server sends a thin HTML shell and a pile of JS, and that JS is where the application actually lives. Which is good for me, because to do its job the frontend has to know every endpoint it can call, every parameter it can send, and sometimes a key or two it should never have been handed. The server will not tell you its routes. Its own JavaScript will. This is JavaScript recon, and on any modern target it is one of the highest-value hours I spend. Here is how I run it.

Why is the frontend such a good source?

A single-page app, React or Angular or Vue, is just a client that talks to a backend API. For that client to talk to the API, the API's shape has to be baked into the shipped JavaScript: the base URL, the paths, the parameter names, the headers it sets, the roles it checks. All of that sits in files anyone can download. So the JS is a map of the backend, handed to you for free. The homepage shows you the three features the product team wants you to see. The JavaScript shows you the forty endpoints behind them, including the ones no button ever calls.

Step 1: Collect every JS file

Before I read anything, I gather all of it, and "all" is bigger than the files loaded on the page right now.

  • What the page loads now. Crawl the app and collect every script source, including the webpack chunks that load lazily as you click around. katana, hakrawler, and gospider do the crawling; getJS and subjs pull script URLs out of a page.
  • What it loaded in the past. Old JS files sit on the server and in archives long after the UI stops referencing them, and old files often hold endpoints that were pulled from the interface but still work on the backend. gau, waybackurls, and waymore pull historical .js URLs from the Wayback Machine and other archives.
  • Across all their hosts. This is where JS recon meets subdomain enumeration. Once you have the full list of hosts, you mine the JS on each one, not just the main app.
echo https://app.example.com | katana -jc -silent \
  | grep '\.js' | sort -u > js-urls.txt

cat js-urls.txt | while read u; do
  curl -s "$u" -o "js/$(echo "$u" | md5sum | cut -c1-12).js"
done

Step 2: Make it readable

Production JS is minified into one unreadable line, sometimes obfuscated on top of that. Two things help.

  • Beautify it. js-beautify or prettier turns the wall of characters back into something with line breaks and indentation you can actually search.
  • Find the source maps. This is the big one. Build tools generate .map files that hold the original, unminified source: real variable names, comments, the folder structure, all of it. If those .map files got shipped to production, and they often do by accident, you can rebuild the app's original source. Look for a //# sourceMappingURL comment at the end of a JS file, or just try adding .map to the file name. unwebpack-sourcemap reconstructs the original file tree from them.
# is there a source map?
curl -s https://app.example.com/main.abc123.js | tail -c 200
# try to grab it
curl -s https://app.example.com/main.abc123.js.map -o main.js.map

A shipped source map turns a black-box frontend into a white-box one. It is one of the most useful things you can find early, so I always check for it before spending time reading minified code.

Step 3: Pull the endpoints out

Now the actual mining. The first thing I want is the API surface: every path the JS references.

  • LinkFinder and xnLinkFinder run patterns over the JS to extract relative and absolute URLs, API paths, and endpoints. xnLinkFinder is good at chewing through a whole folder of files and also pulling likely parameter names.
  • jsluice parses the JavaScript properly instead of regexing it, so it catches URLs and secrets that pattern matching misses.
xnLinkFinder -i js/ -sf app.example.com -o endpoints.txt
# or a single file
python3 linkfinder.py -i main.js -o cli

What comes back is a list of paths like /api/v2/users/{id}, /admin/export, /internal/feature-flags. Some of these the UI uses. Some it does not, and those are the interesting ones: an admin export the frontend never links to, an old v1 endpoint still mounted, an internal route that assumed nobody would find it. I resolve each one against the host with httpx to see which are live, then start testing them for the usual, broken access control, IDOR, missing auth.

Step 4: Hunt for secrets

The frontend is public, so anything hardcoded in it is public too. Developers forget this constantly. What leaks:

  • API keys and tokens: Google Maps keys, Stripe keys, AWS keys, Firebase config, Algolia and Mapbox tokens, third-party service credentials.
  • JWTs and session hints, and now and then a hardcoded test login.
  • Internal hostnames, S3 bucket names, and cloud endpoints that widen the whole target.

SecretFinder and jsluice are the JS-specific tools; trufflehog and gitleaks catch broad secret patterns. The part that takes judgement is impact. A Stripe publishable key is meant to be public and is not a finding. An AWS secret key, or a Google API key with no restrictions, is. So I always test a found key to see what it actually unlocks before I call it anything in a report.

python3 SecretFinder.py -i main.js -o cli
cat js/*.js | jsluice secrets

Step 5: Parameters and client-side sinks

Two more things worth pulling out of the JavaScript.

  • Hidden parameters. The code references parameter names the UI never shows you: debug, admin, isInternal, redirect. Feed those to a tool like Arjun, or just try them by hand. A redirect parameter you found in JS is a strong open-redirect and SSRF lead.
  • DOM sinks. This is where recon turns into client-side bug hunting. I grep for the dangerous sinks, innerHTML, document.write, eval, and postMessage handlers, then trace backwards to see whether input I control can reach one. That is the road to DOM-based XSS, which behaves differently from the reflected kind in the XSS writeup, because the payload never touches the server at all.

Step 6: Watch it change over time

Here is what makes JS recon different from most recon: the source updates every time the company deploys. A new feature ships as new JavaScript, often days or weeks before there is any UI or announcement for it. So I monitor the JS files on a schedule and diff them. When a fresh bundle adds an endpoint or a parameter, I see it the moment it lands, sometimes before it is meant to be reachable. JSMon does this, or a simple cron that fetches, beautifies, and diffs. It turns a one-time look into recon that runs itself.

The toolbox

  • Collect: katana, hakrawler, gospider, getJS, subjs, gau, waybackurls, waymore.
  • Read: js-beautify, prettier, unwebpack-sourcemap for source maps.
  • Endpoints: LinkFinder, xnLinkFinder, jsluice.
  • Secrets: SecretFinder, jsluice, trufflehog, gitleaks.
  • Parameters: Arjun, plus the param lists xnLinkFinder pulls.
  • Resolve and test: httpx to see which endpoints are live, then your proxy of choice.
  • Monitor: JSMon, or a cron of fetch plus beautify plus diff.

What does JS recon actually find?

The payoffs are some of the best in web testing. Undocumented endpoints that skipped the access-control review because nobody thought they were reachable. An API built for the mobile app that trusts the client more than the web version does. A leaked key that opens a cloud bucket. A DOM XSS buried in a postMessage handler. A staging feature flag you can flip on for yourself. All of it comes from reading files the app handed you on the way in.

The defender's side

If you build web apps, assume everything you ship to the browser is public, because it is. Keep secrets on the backend, never in frontend config. Strip source maps from production builds, or you are shipping your original source code to everyone who visits. And remember that pulling a feature from the UI does not pull its endpoint off the server, or its old JavaScript out of the archives. The frontend is the most-read file you publish. Read it like an attacker will, before one does.

JavaScript recon is reading the map the application drew for you. Between this and subdomain enumeration, most of a web target's real attack surface is sitting in public before you send a single malicious request. If you want yours mapped and tested the way an attacker would, that is the work I do.