
Finde jede API-Anfrage, die eine Web-App stellt, anhand des von ihr ausgelieferten JavaScripts: Methode, URL, Body, Header und GraphQL-Operationen. Ein Befehl.
Jede Anfrage, die eine Web-App zu stellen programmiert ist, aus dem von ihr ausgelieferten JavaScript rekonstruieren: Methode, URL, Body, Content-Type und Header.
Das JavaScript-Bundle einer Single-Page-App enthält jede API-Anfrage, die die App stellen kann. Ausgehend von einer URL findet jshound das Bundle der App, durchsucht es vollständig, einschließlich lazy geladener Chunks, und extrahiert diese Anfragen.
Es ist passiv. Die einzigen Anfragen, die es stellt, sind unauthentifizierte GETs für öffentliche statische Dateien, die Seite und ihr JavaScript. Die Analyse läuft lokal ab, und nichts wird an die API der App gesendet.
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Benötigt Python 3.10 oder neuer und Node 18 oder neuer.
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, wo der Build von supabase liegt.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"}
Mehrere Ziele gleichzeitig, oder eine Liste über stdin:
jshound app.example.com api.example.com
cat scope.txt | jshound
Dateien, die bereits auf der Festplatte liegen, etwa der js/-Ordner eines vorherigen Durchlaufs, werden gelesen, ohne das Netzwerk zu berühren:
$ 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
Alles landet in .jshound/<host>/, oder in -o DIR:
endpoints.txt | die obige Tabelle, eine Anfrage pro Zeile |
requests.jsonl | ein JSON-Objekt pro Anfrage: method, url, body, content_type, headers, graphql oder rpc, sowie die Seite und die Datei, aus der sie stammt |
urls.txt | METHOD URL für die Anfragen, deren Host bekannt ist, für Tools, die eine Liste entgegennehmen |
openapi.json | ein OpenAPI-3-Dokument, zum Importieren in Burp, Caido oder Postman |
js/ | jede Datei, die geladen wurde |
{...} ist ein Wert, den jshound nicht auflösen konnte, geschrieben so, wie der Code ihn schreibt: {baseUrl},
{window.__CONFIG__.apiBaseUrl}. In einem Pfad ist {ref} ein Parameter, und {param_o} ist der
Funktionsparameter o./api/surveys/, durchläuft Code, dem jshound noch nicht bis zu seiner
Basis folgt. requests.jsonl nennt die Datei, aus der er stammt.input: ein Argument, state: eine Eigenschaft
eines Objekts, computed: das Ergebnis eines Aufrufs, ? unbekannt.application/json? ist ein Content-Type, der aus den übrigen Anfragen der Datei abgeleitet wurde, statt an
der Aufrufstelle gelesen zu werden. ~{...} ist ein Body, der mehr Felder hat als die aufgeführten.{graphql} ist eine GraphQL-Operation, mit ihrem Namen, Typ und ihren Variablen als Body.| bundler | lazy chunk URLs | module tree | how |
|---|---|---|---|
| webpack 5 | ja | ja | liest __webpack_require__.u aus dem Syntaxbaum |
| webpack 4 | ja | ja | hat kein .u, daher wird der benannte URL-Builder über seine Form statt über seinen Namen erkannt |
| Rspack | ja | ja | webpack-kompatibel, gibt üblicherweise ESM aus |
| Turbopack | ja | n/a | überhaupt kein Builder; die Chunk-Liste ist ein literales Array |
| Vite / Rollup | ja | n/a | Pfade werden niedergeschrieben (__vite__mapDeps); flaches ESM hat keine Registry zum Durchlaufen |
| esbuild | ja | n/a | dasselbe |
| Parcel | ja | n/a | die Runtime-Globalvariable erhält pro Build ein Suffix (parcelRequire5707) und die Chunk-Tabelle ist ein Objekt, kein Array |
| Metro (React Native) | n/a | n/a | liefert ein einziges Bundle, keine Chunks; erkannt an __METRO_GLOBAL_PREFIX__ |
| webpack Module Federation | ja | ja | der Builder trägt keine ID-Map, daher werden IDs aus den Aufrufstellen von __webpack_require__.e(480) entnommen |
jshound ruft das Ziel auf, das Sie ihm geben, und die JavaScript-Dateien, die die Seiten dieses Ziels laden, wo auch immer sie gehostet werden. Bei supabase ist das größtenteils frontend-assets.supabase.com. Dies sind öffentliche statische Dateien, die der Browser jedes Besuchers herunterlädt. Diese anderen Hosts bekommen nichts außer diesen GETs.
Es lehnt jede URL ab, die ein Ziel im privaten Adressraum nennt (Loopback, RFC1918, Link-Local), es sei denn, das Ziel, das Sie ihm gegeben haben, ist selbst privat oder Sie übergeben --allow-private.
-o DIR | wohin die Ergebnisse gehen (Standard .jshound/<host>) |
-q | jede Anfrage nicht ausgeben, während sie gestellt wird |
-v | zusätzlich das Analyse-Log ausgeben |
--max-chunks N | nach dieser Anzahl von lazy Chunks und Imports abbrechen (Standard 50000) |
--no-chunks | lazy Chunks nicht verfolgen |
--proxy URL | jede Anfrage über einen Proxy senden (oder JSHOUND_PROXY setzen) |
--proxy-ca FILE | das CA-Zertifikat eines Proxys, der TLS abfängt |
--client PROFILE | ein einzelnes Browser-TLS-Profil festlegen, statt bei Blockierung zu rotieren |
--allow-private | URLs im privaten Adressraum erlauben (siehe Geltungsbereich) |