
PoC — abuso de proxy cross-origin de chaves de API de provedores configuradas no PasteGuard (GHSA-q94x-p9rc-q89f, CVE-2026-86998, CVSS 7.6).
Status do CVE: solicitado, aguardando atribuição. Esta descoberta é publicada como GHSA-q94x-p9rc-q89f. Após a atribuição do CVE, este repositório é renomeado
CVE-YYYY-NNNNN-pasteguard-PoCe este banner é substituído pelo link do CVE.
| Pesquisador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-q94x-p9rc-q89f |
| CVSS 3.1 | 7.6 (Alto) |
| Fraqueza | CWE-352, CWE-942 |
Resumo: A ausência de proteção CORS/CSRF nas rotas de proxy LLM no PasteGuard 0.9.1 permite que um atacante remoto (qualquer site que o navegador do operador visite, ou qualquer host na rede local) dispare requisições autenticadas à API OpenAI/Anthropic configurada pelo operador usando a chave de API de fallback do lado do servidor do PasteGuard, e leia a resposta, por meio de um fetch() de origem cruzada para /openai/v1/chat/completions (e a rota irmã /anthropic/v1/messages).
Produto: PasteGuard (github.com/sgasser/pasteguard)
Versão testada: commit 100718191499c52934f3b2ccca72ca16373fb765 (2026-07-30), versão do package.json 0.9.1
CVSS v3.1 estimado: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:H — 7.6 (Alto)
UI:R — o operador precisa ter seu navegador aberto em uma página (qualquer página, em qualquer site) enquanto o PasteGuard está em execução; nenhuma outra interação é necessária.C:L / I:L — o JS do próprio atacante lê apenas a resposta ao seu próprio prompt escolhido pelo atacante (não as conversas anteriores/painel do operador, que estão corretamente excluídos do CORS), mas isso ainda confirma/usa uma conta ativa com créditos e polui o histórico de uso/auditoria da conta sob a identidade do operador.A:H — o impacto realista e ilimitado: uma página do atacante pode repetir essa requisição indefinidamente, gerando uso real cobrado contra a conta/cota do provedor do operador (dreno financeiro, possível esgotamento de limite de taxa ou sinalizações de abuso do lado do provedor) sem qualquer limitação de taxa ou verificação de origem no caminho.S:U — o impacto permanece dentro do mesmo limite de confiança (a credencial configurada do próprio proxy é o que é usada); nenhuma autoridade de segurança separada é cruzada.Detalhes:
A camada HTTP do PasteGuard aplica uma política CORS permissiva com curinga a todas as rotas, exceto o painel:
src/index.ts:40-45 — const corsMiddleware = cors(); (o cors() do Hono sem opções tem como padrão Access-Control-Allow-Origin: *) é aplicado via app.use("*", ...) a tudo, com uma exceção explícita apenas para /dashboard e /dashboard/*. O comentário logo acima (src/index.ts:34-39) mostra que o autor raciocinou cuidadosamente sobre a exposição CORS do painel, mas não estendeu o mesmo raciocínio às rotas de proxy abaixo dele.src/config.ts:153 — host: z.string().default("0.0.0.0"), confirmado em execução em config.example.yaml:13. O proxy escuta em todas as interfaces por padrão, não apenas no loopback, então isso também é alcançável a partir da LAN, não apenas do navegador do próprio operador.src/providers/openai/client.ts:48-53:
Se a requisição recebida não carregar um cabeçalho , o PasteGuard anexa silenciosamente sua própria mantida no servidor à requisição de saída. documenta isso como um recurso de conveniência intencional ("Fallback opcional se o cliente não enviar cabeçalho de autenticação") para o caso de uso "Apps & APIs".Como a política CORS com curinga fica na frente de rotas que detêm essa credencial de fallback, qualquer origem pode (1) fazer o navegador enviar a requisição sem cabeçalho Authorization, fazendo o servidor anexar sua própria chave de API real, e (2) ler a resposta JSON, já que Access-Control-Allow-Origin: * está presente. Nenhuma verificação de Origin/Referer ou token CSRF protege essas rotas.
Prova de Conceito (confirmada dinamicamente, não apenas rastreada estaticamente):
config.yaml com providers.openai.base_url: http://127.0.0.1:9091 (upstream simulado) e providers.openai.api_key: "sk-VICTIM-SECRET-DO-NOT-LEAK-12345", pii_detection.enabled: false (detector simulado apenas em /health, para evitar a necessidade do serviço completo do modelo GLiNER), secrets_detection.enabled: false. Iniciou-se o PasteGuard com bun run src/index.ts, confirmou-se /health → 200.mock_upstream.py) em 127.0.0.1:9091 que registra qualquer cabeçalho Authorization que receba.Evidência bruta: ~/engagements/pasteguard/evidence/cross_origin_csrf_success.png, ~/engagements/pasteguard/evidence/mock_upstream_log.txt.
A rota /anthropic/v1/messages foi confirmada estaticamente (arquivo:linha acima) como compartilhando o padrão idêntico de fallback + CORS com curinga, mas não foi executada separadamente através do PoC do navegador ao vivo, no interesse da orientação da sessão de profundidade sobre amplitude/regra dos 5 minutos, uma vez que a causa raiz já havia sido confirmada em /openai.
Impacto: Qualquer site que o navegador do operador do PasteGuard visite (anúncio malicioso, site comprometido, ou um atacante na mesma LAN, dado o bind padrão em 0.0.0.0) pode silenciosamente disparar requisições cobradas ilimitadas através da própria conta OpenAI/Anthropic do operador via sua instância local do PasteGuard, sem interação do usuário além de ter a aba aberta e sem que o operador possa perceber, a menos que esteja observando o painel de faturamento do seu provedor. Este é um vetor direto de abuso financeiro / esgotamento de cota e, secundariamente, polui o histórico de uso/auditoria da conta do provedor do operador com conteúdo escolhido pelo atacante.
Fraquezas:
Remediação (sugestões):
cors() com curinga a /openai, /anthropic, /codex (e /api/mask se algum dia for estendido para usar credenciais mantidas no servidor) quando uma api_key de fallback estiver configurada. No mínimo, tornar as origens permitidas para essas rotas explícitas/configuráveis (por exemplo, apenas a origem conhecida da extensão do navegador), com padrão de nenhum acesso de origem cruzada em vez de *./dashboard em src/index.ts.server.host de 0.0.0.0 para 127.0.0.1 em src/config.ts:153 e , exigindo uma adesão explícita para fazer bind em todas as interfaces.Crédito: Dostxodjayev Abdullox
Canal de Reporte: Não existe SECURITY.md no repositório (confirmado via gh api repos/sgasser/pasteguard/contents/SECURITY.md → 404, reverificado no momento da auditoria). Também não há avisos de segurança publicados anteriormente (gh api repos/sgasser/pasteguard/security-advisories → []), então isso não parece ser uma duplicata de um problema divulgado anteriormente. Canal recomendado: o fluxo padrão de reporte privado de vulnerabilidades do GitHub em https://github.com/sgasser/pasteguard/security/advisories/new.
// 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}`;
}
Authorizationproviders.openai.api_keyconfig.example.yaml:22-24src/providers/anthropic/client.ts:37-61 implementa o padrão de fallback idêntico para /anthropic/v1/messages (fallback de x-api-key/Authorization para config.api_key) — confirmado como uma instância irmã da mesma causa raiz, não reverificado independentemente de ponta a ponta (ver escopo do PoC abaixo).http://127.0.0.2:8001/attack.htmllocalhostpython3 -m http.server 8001 --bind 127.0.0.2fetch("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. Confirmou-se via inspeção de rede do DevTools:
origin: http://127.0.0.2:8001, sec-fetch-site: cross-site, nenhum cabeçalho Authorization enviado pela página do atacante.access-control-allow-origin: *, HTTP 200, corpo JSON totalmente legível pelo JS da página do atacante.../evidence/cross_origin_csrf_success.png.RECEIVED AUTH HEADER: Bearer sk-VICTIM-SECRET-DO-NOT-LEAK-12345 — provando que o PasteGuard anexou a chave real configurada do operador no lado do servidor, sem que a página do atacante jamais a conhecesse ou a fornecesse.config.example.yaml:13