
Найдите все API-запросы, которые выполняет веб-приложение, по его JavaScript-коду: метод, URL, тело, заголовки и операции GraphQL. Одна команда.
Восстановите каждый запрос, который веб-приложение создано выполнять, из поставляемого им JavaScript: метод, URL, тело, тип содержимого и заголовки.
JavaScript-бандл одностраничного приложения содержит каждый API-запрос, который приложение может выполнить. Получив URL, jshound находит бандл приложения, обходит его целиком, включая лениво загружаемые чанки, и извлекает эти запросы.
Он пассивен. Единственные запросы, которые он делает, — это неаутентифицированные GET-запросы к публичным статическим файлам, странице и её JavaScript. Анализ выполняется локально, и ничего не отправляется в API приложения.
git clone https://github.com/AmbitSecurity/jshound && cd jshound
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Требуется Python 3.10 или новее и Node 18 или новее.
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, где находится сборка 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"}
Несколько целей сразу или список на stdin:
jshound app.example.com api.example.com
cat scope.txt | jshound
Файлы, уже находящиеся на диске, например папка js/ из предыдущего запуска, читаются без обращения к сети:
$ 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
Всё попадает в .jshound/<host>/ или в -o DIR:
endpoints.txt | таблица выше, один запрос на строку |
requests.jsonl | один JSON-объект на запрос: method, url, body, content_type, headers, graphql или rpc, а также страница и файл, из которого он пришёл |
urls.txt | METHOD URL для запросов, чей хост известен, для инструментов, принимающих список |
openapi.json | документ OpenAPI 3 для импорта в Burp, Caido или Postman |
js/ | каждый загруженный файл |
{...} — это значение, которое jshound не смог разрешить, записанное так, как его пишет код: {baseUrl},
{window.__CONFIG__.apiBaseUrl}. В пути {ref} — это параметр, а {param_o} — это
параметр функции o./api/surveys/, проходит через код, который jshound пока не отслеживает до его
базы. requests.jsonl указывает файл, из которого он пришёл.input: — аргумент, state: — свойство
объекта, computed: — результат вызова, ? — неизвестно.application/json? — это тип содержимого, выведенный из других запросов файла, а не прочитанный
в месте вызова. ~{...} — это тело, в котором полей больше, чем перечислено.{graphql} — это операция GraphQL с её именем, типом и переменными в качестве тела.| сборщик | URL ленивых чанков | дерево модулей | как |
|---|---|---|---|
| webpack 5 | да | да | читает __webpack_require__.u из синтаксического дерева |
| webpack 4 | да | да | не имеет .u, поэтому именованный построитель URL сопоставляется по форме, а не по имени |
| Rspack | да | да | совместим с webpack, обычно выдаёт ESM |
| Turbopack | да | н/д | построителя вообще нет; список чанков — это литеральный массив |
| Vite / Rollup | да | н/д | пути записаны явно (__vite__mapDeps); плоский ESM не имеет реестра для обхода |
| esbuild | да | н/д | то же самое |
| Parcel | да | н/д | глобальная переменная времени выполнения имеет суффикс для каждой сборки (parcelRequire5707), а таблица чанков — это объект, а не массив |
| Metro (React Native) | н/д | н/д | поставляет один бандл, без чанков; определяется по __METRO_GLOBAL_PREFIX__ |
| webpack Module Federation | да | да | построитель не несёт карту id, поэтому id берутся из мест вызова __webpack_require__.e(480) |
jshound запрашивает указанную вами цель и JavaScript-файлы, которые загружают страницы этой цели, где бы
они ни размещались. Для supabase это в основном frontend-assets.supabase.com. Это публичные статические
файлы, которые скачивает браузер любого посетителя. Эти другие хосты не получают ничего, кроме этих GET-запросов.
Он отказывается от любого URL, который цель указывает в частном адресном пространстве (loopback, RFC1918, link local), если
только указанная вами цель сама не является частной или вы не передали --allow-private.
-o DIR | куда попадают результаты (по умолчанию .jshound/<host>) |
-q | не выводить каждый запрос по мере его выполнения |
-v | также выводить журнал анализа |
--max-chunks N | остановиться после такого количества ленивых чанков и импортов (по умолчанию 50000) |
--no-chunks | не следовать за ленивыми чанками |
--proxy URL | отправлять каждый запрос через прокси (или задать JSHOUND_PROXY) |
--proxy-ca FILE | сертификат CA прокси, перехватывающего TLS |
--client PROFILE | закрепить один TLS-профиль браузера вместо ротации при блокировке |
--allow-private | разрешить URL в частном адресном пространстве (см. Область действия) |