
PoC — cross-origin proxy abuse of configured provider API keys in PasteGuard (GHSA-q94x-p9rc-q89f, CVE-2026-86998, CVSS 7.6).
CVE status: requested, pending assignment. This finding is published as GHSA-q94x-p9rc-q89f. On CVE assignment this repository is renamed
CVE-YYYY-NNNNN-pasteguard-PoCand this banner is replaced with the CVE link.
| Researcher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-q94x-p9rc-q89f |
| CVSS 3.1 | 7.6 (High) |
| Weakness | CWE-352, CWE-942 |
Summary: Missing CORS/CSRF protection on the LLM-proxy routes in PasteGuard 0.9.1 allows a remote attacker (any website the operator's browser visits, or any host on the local network) to trigger authenticated requests to the operator's configured OpenAI/Anthropic API using PasteGuard's server-side fallback API key, and read the response, via a cross-origin fetch() to /openai/v1/chat/completions (and sibling /anthropic/v1/messages).
Product: PasteGuard (github.com/sgasser/pasteguard)
Tested Version: commit 100718191499c52934f3b2ccca72ca16373fb765 (2026-07-30), package.json version 0.9.1
Estimated CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:H — 7.6 (High)
UI:R — the operator must have their browser open a page (any page, on any site) while PasteGuard is running; no other interaction is needed.C:L / I:L — the attacker's own JS reads only the response to its own attacker-chosen prompt (not the operator's past conversations/dashboard data, which is correctly excluded from CORS), but this still confirms/uses a live funded account and pollutes the account's usage/audit trail under the operator's identity.A:H — the realistic, unbounded impact: an attacker page can loop this request indefinitely, driving real billed usage against the operator's provider account/quota (financial drain, possible rate-limit exhaustion or provider-side abuse flags) with no rate limiting or origin check in the way.S:U — impact stays within the same trust boundary (the proxy's own configured credential is what gets used); no separate security authority is crossed.Details:
PasteGuard's HTTP layer applies a permissive, wildcard CORS policy to every route except the dashboard:
src/index.ts:40-45 — const corsMiddleware = cors(); (Hono's cors() with no options defaults to Access-Control-Allow-Origin: *) is applied via app.use("*", ...) to everything, with an explicit carve-out only for /dashboard and /dashboard/*. The comment directly above (src/index.ts:34-39) shows the author reasoned carefully about dashboard CORS exposure but did not extend the same reasoning to the proxy routes below it.src/config.ts:153 — host: z.string().default("0.0.0.0"), confirmed live in config.example.yaml:13. The proxy listens on all interfaces by default, not just loopback, so this is reachable from the LAN too, not only from the operator's own browser.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 header, PasteGuard silently attaches its own server-held providers.openai.api_key to the outbound request. config.example.yaml:22-24 documents this as an intentional convenience feature ("Optional fallback if client doesn't send auth header") for the "Apps & APIs" use case.src/providers/anthropic/client.ts:37-61 implements the identical fallback pattern for /anthropic/v1/messages (x-api-key/Authorization fallback to config.api_key) — confirmed as a sibling instance of the same root cause, not independently re-verified end-to-end (see PoC scope below).Because the wildcard CORS policy sits in front of routes that hold this fallback credential, any origin can (1) make the browser send the request with no Authorization header, causing the server to attach its own real API key, and (2) read the JSON response, since Access-Control-Allow-Origin: * is present. No Origin/Referer check or CSRF token guards these routes.
Proof of Concept (dynamically confirmed, not just statically traced):
config.yaml with providers.openai.base_url: http://127.0.0.1:9091 (mock upstream) and providers.openai.api_key: "sk-VICTIM-SECRET-DO-NOT-LEAK-12345", pii_detection.enabled: false (mocked detector /health only, to avoid needing the full GLiNER model service), secrets_detection.enabled: false. Started PasteGuard with bun run src/index.ts, confirmed /health → 200.mock_upstream.py) on 127.0.0.1:9091 that logs whatever Authorization header it receives.http://127.0.0.2:8001/attack.html (different literal loopback IP, per Chrome's site-isolation rules — not a same-origin localhost test), via python3 -m http.server 8001 --bind 127.0.0.2. The page's only action on load:
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. Confirmed via DevTools network inspection:
origin: http://127.0.0.2:8001, sec-fetch-site: cross-site, no Authorization header sent by the attacker page.access-control-allow-origin: *, HTTP 200, JSON body fully readable by the attacker page's JS.../evidence/cross_origin_csrf_success.png.RECEIVED AUTH HEADER: Bearer sk-VICTIM-SECRET-DO-NOT-LEAK-12345 — proving PasteGuard attached the operator's real configured key server-side, without the attacker page ever knowing or supplying it.Raw evidence: ~/engagements/pasteguard/evidence/cross_origin_csrf_success.png, ~/engagements/pasteguard/evidence/mock_upstream_log.txt.
The /anthropic/v1/messages route was confirmed statically (file:line above) to share the identical fallback + wildcard-CORS pattern but was not separately re-run through the live browser PoC, in the interest of the session's depth-over-breadth/5-minute-rule guidance once the root cause was already confirmed on /openai.
Impact: Any website the PasteGuard operator's browser visits (malicious ad, compromised site, or an attacker on the same LAN given the default 0.0.0.0 bind) can silently drive unlimited billed requests through the operator's own OpenAI/Anthropic account via their local PasteGuard instance, with no user interaction beyond having the tab open and no way for the operator to notice short of watching their provider billing dashboard. This is a direct financial-abuse / quota-exhaustion vector, and secondarily pollutes the operator's provider account usage/audit trail with attacker-chosen content.
Weaknesses:
Remediation (suggestions):
cors() middleware to /openai, /anthropic, /codex (and /api/mask if it's ever extended to use server-held credentials) when a fallback api_key is configured. At minimum, make the allowed origins for these routes explicit/configurable (e.g. only the browser extension's known origin), defaulting to no cross-origin access rather than *./dashboard in src/index.ts.server.host default from 0.0.0.0 to 127.0.0.1 in src/config.ts:153 and config.example.yaml:13, requiring an explicit opt-in to bind on all interfaces.Credit: Dostxodjayev Abdullox
Reporting Channel: No SECURITY.md exists in the repository (confirmed via gh api repos/sgasser/pasteguard/contents/SECURITY.md → 404, re-checked at audit time). No prior published security advisories either (gh api repos/sgasser/pasteguard/security-advisories → []), so this does not appear to be a duplicate of a previously disclosed issue. Recommended channel: GitHub's default private vulnerability reporting flow at https://github.com/sgasser/pasteguard/security/advisories/new.