$npx -y skills add elementalsouls/Claude-BugHunter --skill hunt-shadow-apiHunt shadow / zombie / undocumented API surface (OWASP API9 Improper Inventory Management) — enumerate the full API version history (v1/v2/beta/legacy paths, header- and subdomain-based versioning), pull and diff every reachable OpenAPI/Swagger spec (including ones only findable
| 1 | ## OWASP API9 — Improper Inventory Management (Shadow / Zombie APIs) |
| 2 | |
| 3 | As an API evolves, old versions and internal/staging routes routinely stay reachable without |
| 4 | receiving the same security fixes as the current version — because nobody tracks that they |
| 5 | still exist. The bug is rarely in one endpoint; it's in the **delta** between what an old |
| 6 | version enforces and what the current version enforces on the same operation. |
| 7 | |
| 8 | ### When to use |
| 9 | |
| 10 | Trigger when: |
| 11 | - Versioned paths are visible (`/v1/`, `/v2/`, `/api/2023-01-01/`) or `Accept`/`X-API-Version` |
| 12 | headers are in play. |
| 13 | - A changelog, release notes, or deprecation notice references removed/old API behavior. |
| 14 | - A mobile APK/IPA (via `apk-redteam-pipeline` / `ios-redteam-pipeline`) hardcodes endpoints |
| 15 | that look like an older backend version than the current web app calls. |
| 16 | - Multiple OpenAPI/Swagger specs are discoverable, or `info.version` in one spec implies others |
| 17 | exist. |
| 18 | |
| 19 | DO NOT use for single-version APIs with no version history — there's nothing to diff; go |
| 20 | straight to `hunt-api-misconfig` for direct exploitation of the one surface that exists. |
| 21 | |
| 22 | --- |
| 23 | |
| 24 | ## Stage 1 — Enumerate the Full Version Surface |
| 25 | |
| 26 | ```bash |
| 27 | # Path-based versioning |
| 28 | for v in v1 v2 v3 v4 beta alpha internal legacy old 2022-01-01 2023-01-01 2024-01-01; do |
| 29 | curl -s -o /dev/null -w "%{http_code} /api/$v/\n" "https://$TARGET/api/$v/" |
| 30 | done |
| 31 | |
| 32 | # Header-based versioning |
| 33 | curl -s -H "X-API-Version: 1" https://$TARGET/api/users |
| 34 | curl -s -H "Accept: application/vnd.company.v1+json" https://$TARGET/api/users |
| 35 | |
| 36 | # Subdomain-based versioning |
| 37 | for sub in api api-v1 api-v2 apiv1 apiv2 legacy-api old-api internal-api staging-api; do |
| 38 | curl -s -o /dev/null -w "%{http_code} $sub\n" "https://$sub.$TARGET/" |
| 39 | done |
| 40 | ``` |
| 41 | |
| 42 | A `200`/`401`/`403` on an old version path (anything but `404`/connection-refused) means the |
| 43 | version is still live and worth carrying into Stage 3, even if it demands auth. |
| 44 | |
| 45 | --- |
| 46 | |
| 47 | ## Stage 2 — Pull Every Reachable Spec, Not Just the Linked One |
| 48 | |
| 49 | ```bash |
| 50 | for path in openapi.json swagger.json v1/swagger.json v2/swagger.json v3/api-docs \ |
| 51 | api-docs.json swagger/v1/swagger.json .well-known/openapi.json; do |
| 52 | curl -s -o /dev/null -w "%{http_code} /$path\n" "https://$TARGET/$path" |
| 53 | done |
| 54 | |
| 55 | # Wayback Machine — a DEPRECATED version's spec often stays indexed after the live link is removed |
| 56 | curl -s "http://web.archive.org/cdx/search/cdx?url=$TARGET/*swagger*&output=json&collapse=urlkey" |
| 57 | curl -s "http://web.archive.org/cdx/search/cdx?url=$TARGET/*openapi*&output=json&collapse=urlkey" |
| 58 | ``` |
| 59 | |
| 60 | When more than one spec resolves (a current one and an archived/old one), diff the endpoint |
| 61 | inventories directly: |
| 62 | ```bash |
| 63 | jq -r '.paths | keys[]' v1-swagger.json | sort > /tmp/v1_paths.txt |
| 64 | jq -r '.paths | keys[]' v2-swagger.json | sort > /tmp/v2_paths.txt |
| 65 | comm -23 /tmp/v1_paths.txt /tmp/v2_paths.txt # in v1 only — candidates for "still live but forgotten" |
| 66 | ``` |
| 67 | For every path in that diff, confirm it's still reachable against the v1 base URL. A route |
| 68 | documented only in the old spec that still returns something other than `404` is a zombie- |
| 69 | endpoint candidate — carry it into Stage 3. |
| 70 | |
| 71 | --- |
| 72 | |
| 73 | ## Stage 3 — Behavioral Diff Between Old and Current Version |
| 74 | |
| 75 | For each operation that exists in **both** versions, compare security-relevant behavior, not |
| 76 | response shape. Response shape differences are Informational; behavioral security regressions |
| 77 | are the finding. |
| 78 | |
| 79 | - **Auth strength.** Does the old version accept no token, an expired token, or a lower- |
| 80 | privilege token that the current version rejects? |
| 81 | ```bash |
| 82 | curl -s -H "Authorization: Bearer $EXPIRED_TOKEN" https://$TARGET/api/v1/users/me -w '\n%{http_code}\n' |
| 83 | curl -s -H "Authorization: Bearer $EXPIRED_TOKEN" https://$TARGET/api/v2/users/me -w '\n%{http_code}\n' |
| 84 | ``` |
| 85 | - **Rate limiting.** Burst the same number of requests against both versions' equivalent |
| 86 | endpoint; a missing `429` on the old version means rate-limiting was added later and never |
| 87 | backport |