
Trova ogni richiesta API effettuata da un'app web, dal JavaScript che distribuisce: metodo, URL, body, header e operazioni GraphQL. Un solo comando.
Recupera ogni richiesta che una web app è progettata per effettuare dal JavaScript che distribuisce: metodo, URL, body, content type e header.
Il bundle JavaScript di una single page app contiene ogni richiesta API che l'app può effettuare. Dato un URL, jshound trova il bundle dell'app, lo analizza interamente, inclusi i chunk caricati in modo lazy, ed estrae quelle richieste.
È passivo. Le uniche richieste che effettua sono GET non autenticate per file statici pubblici, la pagina e il suo JavaScript. L'analisi viene eseguita localmente e nulla viene inviato all'API dell'app.
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Richiede Python 3.10 o superiore e Node 18 o superiore.
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, dove risiede la build di 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"}
Più target contemporaneamente, o una lista su stdin:
jshound app.example.com api.example.com
cat scope.txt | jshound
I file già presenti su disco, come la cartella js/ di un'esecuzione precedente, vengono letti senza toccare la rete:
$ 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
Tutto va in .jshound/<host>/, oppure in -o DIR:
endpoints.txt | la tabella sopra, una richiesta per riga |
requests.jsonl | un oggetto JSON per richiesta: method, url, body, content_type, headers, graphql o rpc, e la pagina e il file da cui proviene |
urls.txt | METHOD URL per le richieste il cui host è noto, per strumenti che accettano una lista |
openapi.json | un documento OpenAPI 3, da importare in Burp, Caido o Postman |
js/ | ogni file che è stato recuperato |
{...} è un valore che jshound non è riuscito a risolvere, scritto come lo scrive il codice: {baseUrl},
{window.__CONFIG__.apiBaseUrl}. In un path, {ref} è un parametro, e {param_o} è il
parametro di funzione o./api/surveys/, passa attraverso codice che jshound non segue fino alla sua
base. requests.jsonl indica il file da cui proviene.input: un argomento, state: una proprietà
di un oggetto, computed: il risultato di una chiamata, ? sconosciuto.application/json? è un content type dedotto dalle altre richieste del file anziché letto
nel punto di chiamata. ~{...} è un body che ha più campi di quelli elencati.{graphql} è un'operazione GraphQL, con il suo nome, tipo e variabili come body.| bundler | URL dei chunk lazy | albero dei moduli | come |
|---|---|---|---|
| webpack 5 | sì | sì | legge __webpack_require__.u dall'albero sintattico |
| webpack 4 | sì | sì | non ha .u, quindi il costruttore di URL con nome viene individuato per forma anziché per nome |
| Rspack | sì | sì | compatibile con webpack, di solito emette ESM |
| Turbopack | sì | n/a | nessun costruttore; la lista dei chunk è un array letterale |
| Vite / Rollup | sì | n/a | i path sono scritti esplicitamente (__vite__mapDeps); l'ESM flat non ha un registry da percorrere |
| esbuild | sì | n/a | idem |
| Parcel | sì | n/a | il global del runtime ha un suffisso per build (parcelRequire5707) e la tabella dei chunk è un oggetto, non un array |
| Metro (React Native) | n/a | n/a | distribuisce un solo bundle, nessun chunk; rilevato tramite __METRO_GLOBAL_PREFIX__ |
| webpack Module Federation | sì | sì | il costruttore non porta una mappa di id, quindi gli id vengono presi dai punti di chiamata __webpack_require__.e(480) |
jshound richiede il target che gli fornisci, e i file JavaScript che le pagine di quel target caricano, ovunque
siano ospitati. Per supabase si tratta principalmente di frontend-assets.supabase.com. Questi sono file statici
pubblici che il browser di qualsiasi visitatore scarica. Quegli altri host non ricevono altro che quelle GET.
Rifiuta qualsiasi URL che un target nomini in uno spazio di indirizzi privato (loopback, RFC1918, link local), a meno
che il target che gli hai fornito non sia esso stesso privato o tu non passi --allow-private.
-o DIR | dove vanno i risultati (predefinito .jshound/<host>) |
-q | non stampare ogni richiesta mentre viene effettuata |
-v | stampa anche il log di analisi |
--max-chunks N | fermati dopo questo numero di chunk lazy e import (predefinito 50000) |
--no-chunks | non seguire i chunk lazy |
--proxy URL | invia ogni richiesta attraverso un proxy (oppure imposta JSHOUND_PROXY) |
--proxy-ca FILE | il certificato CA di un proxy che intercetta TLS |
--client PROFILE | fissa un profilo TLS del browser invece di ruotare quando viene bloccato |
--allow-private | consenti URL in spazi di indirizzi privati (vedi Ambito) |