$npx -y skills add elementalsouls/Claude-BugHunter --skill hunt-spa-apiDiscover a single-page-app's hidden backend API from its public JS bundle, then test that API for broken access control / missing authentication. One of the highest-yield web plays in modern recon — SPAs ship their entire backend route map to the browser, and the API behind them
| 1 | ## When to use this skill |
| 2 | |
| 3 | Trigger when: |
| 4 | - A target host returns a tiny HTML shell + big `/static/js/*.js` or `/_next/static/*` bundles (React/Vue/Angular/Next/Svelte SPA) |
| 5 | - You see a subdomain named `console`, `app`, `dashboard`, `portal`, `admin`, `panel`, `manage`, `internal` |
| 6 | - Recon surfaces any `*api*`, `*-api*`, `api.*` host |
| 7 | - A login page is OAuth/SSO-gated (the *frontend* auth tells you nothing about whether the *API* enforces auth) |
| 8 | |
| 9 | The core insight: **a SPA is a client to a backend API, and it ships the full map of that API — hosts, routes, sometimes keys — to anyone who views source.** The login page being protected says nothing about whether the API behind it checks tokens. Auth is frequently enforced on the *gateway/login* and missing on a *route group* of the API. |
| 10 | |
| 11 | DO NOT skip this because "the app needs login" — that's exactly when this pays off. |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## The play (5 steps) |
| 16 | |
| 17 | ### 1. Pull the shell + enumerate the bundles |
| 18 | ```bash |
| 19 | curl -s https://console.target.com/ -o index.html |
| 20 | # React/CRA: |
| 21 | grep -oE '/static/js/[^"]+\.js' index.html |
| 22 | # Next.js: |
| 23 | grep -oE '/_next/static/[^"]+\.js' index.html |
| 24 | # generic: |
| 25 | grep -oiE 'src="[^"]+\.js[^"]*"' index.html |
| 26 | ``` |
| 27 | Download every bundle (they can be multi-MB — that's fine, it's all route data): |
| 28 | ```bash |
| 29 | mkdir bundles |
| 30 | for j in $(grep -oE '/static/js/[^"]+\.js' index.html | sort -u); do |
| 31 | curl -s "https://console.target.com$j" -o "bundles/$(echo "$j"|tr '/' '_')" |
| 32 | done |
| 33 | ``` |
| 34 | |
| 35 | ### 2. Harvest API hosts, routes, and secrets from the bundles |
| 36 | ```bash |
| 37 | B=bundles/*.js |
| 38 | # Backend API hosts (incl. dev/beta/staging variants — often weaker auth) |
| 39 | grep -ohiE 'https://[a-z0-9.-]*(api|console|backend|service)[a-z0-9.-]*\.target\.com[a-z0-9/_-]*' $B | sort -u |
| 40 | # Versioned API base paths |
| 41 | grep -ohiE '/api/v[0-9]+/?' $B | sort -u |
| 42 | # Route literals — minified bundles store routes as STRING segments, not full URLs. |
| 43 | # Grep for quoted "resource/action" strings: |
| 44 | grep -ohiE '"[a-z0-9_-]+/[a-z0-9_/-]+"' $B | tr -d '"' \ |
| 45 | | grep -iE '(login|user|account|order|billing|invoice|payment|deal|report|token|otp|password|reset|admin|profile|auth|upload|export|role|permission|dashboard|wallet|finance|sales)' | sort -u |
| 46 | # Secrets (validate before trusting — most AIza keys are Maps/analytics, not Auth) |
| 47 | grep -ohiE '(AIza[0-9A-Za-z_-]{35}|AKIA[0-9A-Z]{16}|sk_live_[0-9A-Za-z]+|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|apiKey["'"'"']?\s*[:=]\s*["'"'"'][^"'"'"']+)' $B | sort -u |
| 48 | ``` |
| 49 | **Note:** minifiers store routes as concatenated string segments (e.g. `"account/payment/list"`), NOT full `/api/v2/...` URLs — so a naive `/api/v*` grep returns nothing. Grep for the **resource-word route strings** and prepend the base yourself. |
| 50 | |
| 51 | ### 3. Establish a CONTROL — find an endpoint that IS gated |
| 52 | Before declaring anything vulnerable, send an unauthenticated request to an endpoint you expect to be protected, and capture what *correct* rejection looks like: |
| 53 | ```bash |
| 54 | curl -s -X POST https://api.target.com/api/users -H 'Content-Type: application/json' -d '{}' |
| 55 | # secure → {"error":"Missing or invalid authorization header"} or HTTP 401 |
| 56 | ``` |
| 57 | This is your differential. A sibling API (e.g. a second API host, or a different route group on the same host) is the ideal control — same stack, so a different response = real authz gap, not a quirk. |
| 58 | |
| 59 | ### 4. Test each route family UNAUTHENTICATED, both methods |
| 60 | For every discovered route, send it with **no `Authorization` header** and compare to the control: |
| 61 | ```bash |
| 62 | for r in <routes>; do |
| 63 | curl -s -o /tmp/r -w "[%{http_code}] $r\n" -X POST -H 'Content-Type: application/json' -d '{}' "https://api.target.com/api/v2/$r" |
| 64 | done |
| 65 | ``` |
| 66 | Interpret: |
| 67 | - **`401`/`"Missing authorization"`** → gated (correct). Move on. |
| 68 | - **`200` with data** → unauthenticated data exposure. **Finding.** |
| 69 | - **`400 "field X is mandatory"`** → the route processed your request and reached *business-logic validation* without an auth check → **auth bypass; supply the field minimally to confirm.** |
| 70 | - **`200` + verb |