
Encontre todas as requisições de API que um aplicativo web faz, a partir do JavaScript que ele envia: método, URL, corpo, cabeçalhos e operações GraphQL. Um comando.
Recupere cada requisição que uma aplicação web é construída para fazer a partir do JavaScript que ela envia: método, URL, corpo, tipo de conteúdo e cabeçalhos.
O bundle JavaScript de uma aplicação de página única contém cada requisição de API que a aplicação pode fazer. Dada uma URL, o jshound encontra o bundle da aplicação, rastreia todo ele, incluindo chunks carregados sob demanda, e extrai essas requisições.
Ele é passivo. As únicas requisições que faz são GETs não autenticados para arquivos estáticos públicos, a página e seu JavaScript. A análise roda localmente, e nada é enviado para a API da aplicação.
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Requer Python 3.10 ou mais recente e Node 18 ou mais recente.
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, onde o build do supabase fica.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"}
Vários alvos de uma vez, ou uma lista via stdin:
jshound app.example.com api.example.com
cat scope.txt | jshound
Arquivos já em disco, como a pasta js/ de uma execução anterior, são lidos sem tocar na rede:
$ 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
Tudo vai para .jshound/<host>/, ou para -o DIR:
endpoints.txt | a tabela acima, uma requisição por linha |
requests.jsonl | um objeto JSON por requisição: method, url, body, content_type, headers, graphql ou rpc, e a página e o arquivo de onde veio |
urls.txt | METHOD URL para as requisições cujo host é conhecido, para ferramentas que aceitam uma lista |
openapi.json | um documento OpenAPI 3, para importar no Burp, Caido ou Postman |
js/ | cada arquivo que foi buscado |
{...} é um valor que o jshound não conseguiu resolver, escrito como o código o escreve: {baseUrl},
{window.__CONFIG__.apiBaseUrl}. Em um caminho, {ref} é um parâmetro, e {param_o} é o
parâmetro de função o./api/surveys/, passa por código que o jshound ainda não segue até sua
base. requests.jsonl nomeia o arquivo de onde veio.input: um argumento, state: uma propriedade
de um objeto, computed: o resultado de uma chamada, ? desconhecido.application/json? é um tipo de conteúdo inferido a partir das outras requisições do arquivo em vez de lido
no local da chamada. ~{...} é um corpo que tem mais campos do que os listados.{graphql} é uma operação GraphQL, com seu nome, tipo e variáveis como o corpo.| bundler | URLs de chunks sob demanda | árvore de módulos | como |
|---|---|---|---|
| webpack 5 | sim | sim | lê __webpack_require__.u da árvore de sintaxe |
| webpack 4 | sim | sim | não tem .u, então o construtor de URL nomeado é correspondido por forma em vez de por nome |
| Rspack | sim | sim | compatível com webpack, geralmente emitindo ESM |
| Turbopack | sim | n/a | nenhum construtor; a lista de chunks é um array literal |
| Vite / Rollup | sim | n/a | os caminhos são escritos (__vite__mapDeps); ESM plano não tem registro para percorrer |
| esbuild | sim | n/a | o mesmo |
| Parcel | sim | n/a | o global de runtime tem sufixo por build (parcelRequire5707) e a tabela de chunks é um objeto, não um array |
| Metro (React Native) | n/a | n/a | envia um único bundle, sem chunks; detectado por __METRO_GLOBAL_PREFIX__ |
| webpack Module Federation | sim | sim | o construtor não carrega um mapa de ids, então os ids são obtidos dos locais de chamada __webpack_require__.e(480) |
O jshound requisita o alvo que você fornece, e os arquivos JavaScript que as páginas desse alvo carregam, onde quer que
estejam hospedados. Para o supabase isso é principalmente frontend-assets.supabase.com. Esses são arquivos estáticos públicos
que o navegador de qualquer visitante baixa. Esses outros hosts não recebem nada além desses GETs.
Ele recusa qualquer URL que um alvo nomeie em espaço de endereço privado (loopback, RFC1918, link local), a menos que
o alvo que você forneceu seja ele próprio privado ou você passe --allow-private.
-o DIR | onde os resultados vão (padrão .jshound/<host>) |
-q | não imprimir cada requisição conforme é feita |
-v | também imprimir o log de análise |
--max-chunks N | parar após este número de chunks sob demanda e imports (padrão 50000) |
--no-chunks | não seguir chunks sob demanda |
--proxy URL | enviar cada requisição através de um proxy (ou definir JSHOUND_PROXY) |
--proxy-ca FILE | o certificado CA de um proxy que intercepta TLS |
--client PROFILE | fixar um perfil TLS de navegador em vez de alternar quando bloqueado |
--allow-private | permitir URLs em espaço de endereço privado (ver Escopo) |