
PoC — злоупотребление кросс-доменным прокси с использованием настроенных API-ключей провайдеров в PasteGuard (GHSA-q94x-p9rc-q89f, CVE-2026-86998, CVSS 7.6).
Статус CVE: запрошен, ожидает присвоения. Эта находка опубликована как GHSA-q94x-p9rc-q89f. После присвоения CVE этот репозиторий будет переименован в
CVE-YYYY-NNNNN-pasteguard-PoC, а этот баннер заменён ссылкой на CVE.
| Исследователь | Dostxodjayev Abdullox (@squeeze440) |
| Рекомендация | GHSA-q94x-p9rc-q89f |
| CVSS 3.1 | 7.6 (Высокий) |
| Слабость | CWE-352, CWE-942 |
Краткое описание: Отсутствие защиты CORS/CSRF на маршрутах LLM-прокси в PasteGuard 0.9.1 позволяет удалённому злоумышленнику (любому сайту, который посещает браузер оператора, или любому хосту в локальной сети) инициировать аутентифицированные запросы к настроенному оператором API OpenAI/Anthropic, используя серверный резервный API-ключ PasteGuard, и прочитать ответ через кросс-доменный fetch() к /openai/v1/chat/completions (и родственному /anthropic/v1/messages).
Продукт: PasteGuard (github.com/sgasser/pasteguard)
Протестированная версия: коммит 100718191499c52934f3b2ccca72ca16373fb765 (2026-07-30), версия в package.json 0.9.1
Оценка CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:H — 7.6 (Высокий)
UI:R — оператор должен открыть в браузере страницу (любую страницу, на любом сайте), пока PasteGuard запущен; никакого другого взаимодействия не требуется.C:L / I:L — JS злоумышленника читает только ответ на собственный выбранный им запрос (не прошлые разговоры оператора/данные панели управления, которые корректно исключены из CORS), но это всё равно подтверждает/использует живой оплаченный аккаунт и загрязняет историю использования/аудита аккаунта под личностью оператора.A:H — реалистичное, неограниченное воздействие: страница злоумышленника может бесконечно повторять этот запрос, вызывая реальное тарифицируемое использование против аккаунта/квоты провайдера оператора (финансовое истощение, возможное исчерпание лимитов или флаги злоупотребления на стороне провайдера) без каких-либо ограничений скорости или проверки источника на пути.S:U — воздействие остаётся в пределах одной границы доверия (используется собственный настроенный учётный данные прокси); отдельный субъект безопасности не затрагивается.Детали:
HTTP-слой PasteGuard применяет разрешительную, wildcard-политику CORS ко всем маршрутам, кроме панели управления:
src/index.ts:40-45 — const corsMiddleware = cors(); (Hono cors() без опций по умолчанию устанавливает Access-Control-Allow-Origin: *) применяется через app.use("*", ...) ко всему, с явным исключением только для /dashboard и /dashboard/*. Комментарий непосредственно выше (src/index.ts:34-39) показывает, что автор тщательно продумал экспозицию CORS для панели управления, но не распространил те же рассуждения на маршруты прокси ниже.src/config.ts:153 — host: z.string().default("0.0.0.0"), подтверждено в config.example.yaml:13. Прокси по умолчанию слушает на всех интерфейсах, а не только на loopback, поэтому он доступен и из локальной сети, а не только из браузера самого оператора.src/providers/openai/client.ts:48-53:
// Use client's auth header if provided, otherwise fall back to config
if (authHeader) {
headers.Authorization = authHeader;
} else if (config.api_key) {
headers.Authorization = `Bearer ${config.api_key}`;
}
Если входящий запрос не содержит заголовок Authorization, PasteGuard молча прикрепляет свой собственный серверный providers.openai.api_key к исходящему запросу. config.example.yaml:22-24 документирует это как намеренную функцию удобства ("Optional fallback if client doesn't send auth header") для сценария использования "Apps & APIs".src/providers/anthropic/client.ts:37-61 реализует идентичный шаблон резервирования для /anthropic/v1/messages (резервирование x-api-key/Authorization к config.api_key) — подтверждено как родственный экземпляр той же первопричины, но не проверено независимо от начала до конца (см. область PoC ниже).Поскольку wildcard-политика CORS стоит перед маршрутами, которые содержат этот резервный учётный данные, любой источник может (1) заставить браузер отправить запрос без заголовка Authorization, что приведёт к тому, что сервер прикрепит свой собственный реальный API-ключ, и (2) прочитать JSON-ответ, поскольку присутствует Access-Control-Allow-Origin: *. Никакая проверка Origin/Referer или CSRF-токен не защищают эти маршруты.
Доказательство концепции (динамически подтверждено, а не только статически отслежено):
config.yaml с providers.openai.base_url: http://127.0.0.1:9091 (mock upstream) и providers.openai.api_key: "sk-VICTIM-SECRET-DO-NOT-LEAK-12345", pii_detection.enabled: false (только mock-детектор /health, чтобы избежать необходимости в полном сервисе модели GLiNER), secrets_detection.enabled: false. Запущен PasteGuard с bun run src/index.ts, подтверждено /health → 200.mock_upstream.py) на 127.0.0.1:9091, который логирует любой полученный заголовок Authorization.http://127.0.0.2:8001/attack.html (другой литеральный loopback IP, согласно правилам изоляции сайтов Chrome — не тест same-origin localhost), через python3 -m http.server 8001 --bind 127.0.0.2. Единственное действие страницы при загрузке:
fetch("http://127.0.0.1:3000/openai/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: "cross-origin drive-by call, attacker supplied NO api key" }] })
}).then(r => r.json()).then(data => { /* render on page */ });
http://127.0.0.2:8001/attack.html. Подтверждено через инспекцию сети DevTools:
origin: http://127.0.0.2:8001, sec-fetch-site: cross-site, без заголовка Authorization, отправленного страницей злоумышленника.access-control-allow-origin: *, HTTP 200, JSON-тело полностью читаемо JS страницы злоумышленника.../evidence/cross_origin_csrf_success.png.RECEIVED AUTH HEADER: Bearer sk-VICTIM-SECRET-DO-NOT-LEAK-12345 — доказывая, что PasteGuard прикрепил реальный настроенный ключ оператора на стороне сервера, при этом страница злоумышленника никогда не знала и не предоставляла его.Необработанные доказательства: ~/engagements/pasteguard/evidence/cross_origin_csrf_success.png, ~/engagements/pasteguard/evidence/mock_upstream_log.txt.
Маршрут /anthropic/v1/messages был подтверждён статически (файл:строка выше) как имеющий идентичный шаблон резервирования + wildcard-CORS, но не был отдельно повторно запущен через живой браузерный PoC, в интересах руководства сессии о глубине над широтой/правила 5 минут, поскольку первопричина уже была подтверждена на /openai.