
Trouvez chaque requête API effectuée par une application web, à partir du JavaScript qu'elle livre : méthode, URL, corps, en-têtes et opérations GraphQL. Une seule commande.
Récupérez chaque requête qu'une application web est conçue pour émettre à partir du JavaScript qu'elle livre : méthode, URL, corps, type de contenu et en-têtes.
Le bundle JavaScript d'une application monopage contient chaque requête API que l'application peut émettre. À partir d'une URL, jshound trouve le bundle de l'application, le parcourt entièrement, y compris les chunks chargés paresseusement, et extrait ces requêtes.
Il est passif. Les seules requêtes qu'il émet sont des GET non authentifiés pour des fichiers statiques publics, la page et son JavaScript. L'analyse s'exécute localement, et rien n'est envoyé à l'API de l'application.
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Nécessite Python 3.10 ou plus récent et Node 18 ou plus récent.
jshound supabase.com/dashboard
$ jshound supabase.com/dashboard
307 128B https://supabase.com/dashboard
200 19K https://supabase.com/dashboard/org
200 28K https://frontend-assets.supabase.com/studio/8617532ae1a4/_next/static/chunks/1p849pzlxcp4n.js?dpl=dpl_ESaSquAbnWrJqbvvyRh7F1qj5wWH
...
supabase.com 200 2282 files (2281 from frontend-assets.supabase.com, 1 from supabase.com) -> 387 endpoints
1 declared chunk(s) could not be fetched
-> .jshound/supabase.com/endpoints.txt (also requests.jsonl, urls.txt, openapi.json)
frontend-assets.supabase.com, où réside le build de supabase.POST https://api.supabase.com/platform/auth/{ref}/otp {"phone":"state:phone"}
DELETE https://api.supabase.com/v1/projects/{param_n}/functions/{param_e}
DELETE https://supabase.com/dashboard/api/integrations/stripe-sync application/json {"projectRef":"input:e","startTime":"input:t"}
POST https://supabase.com/docs/api/search/fts application/json {"query":"computed:trim"}
Plusieurs cibles à la fois, ou une liste sur stdin :
jshound app.example.com api.example.com
cat scope.txt | jshound
Les fichiers déjà présents sur le disque, comme le dossier js/ d'une exécution précédente, sont lus sans toucher au réseau :
$ jshound examples/demo-app
GET https://demo-app.test/api/feature-flags
POST https://demo-app.test/t/events {"sid":"?","evt":"?"}
DELETE https://demo-app.test/v1/sessions/current
POST {baseUrl}/v1/media multipart/form-data {"file":"file-upload"}
POST {baseUrl}/v1/subscribe application/json {"email":"computed:trim","pageUri":"state:href"}
GET {baseUrl}/v1/users/{param_o}/profile
Tout va dans .jshound/<host>/, ou dans -o DIR :
endpoints.txt | le tableau ci-dessus, une requête par ligne |
requests.jsonl | un objet JSON par requête : method, url, body, content_type, headers, graphql ou rpc, et la page et le fichier d'où elle provient |
urls.txt | METHOD URL pour les requêtes dont l'hôte est connu, pour les outils qui prennent une liste |
openapi.json | un document OpenAPI 3, à importer dans Burp, Caido ou Postman |
js/ | chaque fichier qui a été récupéré |
{...} est une valeur que jshound n'a pas pu résoudre, écrite telle que le code l'écrit : {baseUrl},
{window.__CONFIG__.apiBaseUrl}. Dans un chemin, {ref} est un paramètre, et {param_o} est le
paramètre de fonction o./api/surveys/, passe par du code que jshound ne suit pas encore jusqu'à sa
base. requests.jsonl nomme le fichier d'où il provient.input: un argument, state: une propriété
d'un objet, computed: le résultat d'un appel, ? inconnu.application/json? est un type de contenu déduit des autres requêtes du fichier plutôt que lu
au site d'appel. ~{...} est un corps qui a plus de champs que ceux listés.{graphql} est une opération GraphQL, avec son nom, son type et ses variables comme corps.| bundler | URLs de chunks paresseux | arbre de modules | comment |
|---|---|---|---|
| webpack 5 | oui | oui | lit __webpack_require__.u depuis l'arbre syntaxique |
| webpack 4 | oui | oui | n'a pas de .u, donc le constructeur d'URL nommé est mis en correspondance par forme plutôt que par nom |
| Rspack | oui | oui | compatible webpack, émettant généralement de l'ESM |
| Turbopack | oui | n/a | aucun constructeur du tout ; la liste des chunks est un tableau littéral |
| Vite / Rollup | oui | n/a | les chemins sont écrits en dur (__vite__mapDeps) ; l'ESM plat n'a pas de registre à parcourir |
| esbuild | oui | n/a | idem |
| Parcel | oui | n/a | le global du runtime est suffixé par build (parcelRequire5707) et la table des chunks est un objet, pas un tableau |
| Metro (React Native) | n/a | n/a | livre un seul bundle, pas de chunks ; détecté par __METRO_GLOBAL_PREFIX__ |
| webpack Module Federation | oui | oui | le constructeur ne porte pas de table d'ids, donc les ids sont pris depuis les sites d'appel __webpack_require__.e(480) |
jshound requête la cible que vous lui donnez, et les fichiers JavaScript que les pages de cette cible chargent, où qu'ils soient
hébergés. Pour supabase, il s'agit principalement de frontend-assets.supabase.com. Ce sont des fichiers statiques publics
que le navigateur de n'importe quel visiteur télécharge. Ces autres hôtes ne reçoivent rien d'autre que ces GET.
Il refuse toute URL qu'une cible nomme dans l'espace d'adressage privé (loopback, RFC1918, link local), sauf
si la cible que vous lui avez donnée est elle-même privée ou si vous passez --allow-private.
-o DIR | où vont les résultats (par défaut .jshound/<host>) |
-q | ne pas afficher chaque requête au fur et à mesure |
-v | afficher aussi le journal d'analyse |
--max-chunks N | s'arrêter après ce nombre de chunks paresseux et d'imports (par défaut 50000) |
--no-chunks | ne pas suivre les chunks paresseux |
--proxy URL | envoyer chaque requête via un proxy (ou définir JSHOUND_PROXY) |
--proxy-ca FILE | le certificat CA d'un proxy qui intercepte le TLS |
--client PROFILE | épingler un profil TLS de navigateur au lieu d'alterner en cas de blocage |
--allow-private | autoriser les URLs dans l'espace d'adressage privé (voir Périmètre) |