BACK TO SEARCH
ZamoritaCR/joao-spinecritical

JOÃO spine — FastAPI + MCP server

Personal automation server that dispatches shell commands to tmux sessions on a home server, processes AI pipelines (audio, vision, text), manages an ...

purpose: Personal automation server that dispatches shell cthreat: network exposed
Python · 0 · Jun 10, 2026 · Jun 11, 2026 · GITHUB ↗
RISK SCORE
0/ 100 risk
low findings+10
high findings+125
medium findings+75
capped at100
Indicators — descriptive signals, not vulnerabilities
dynamic-importmain.py:339from importlib.metadata import version as _pkg_version
These are automated indicators of code characteristics detected by regex pattern matching. They are informational, not security verdicts. Some patterns (e.g. telegram, crypto-wallet) may reflect legitimate functionality.
VULNERABILITY ANALYSIS · 12 findings in 12 blocks5 HIGH · 5 MEDIUM
HIGH1 finding
mcp_server.py:515
515@mcp.tool()
516async def joao_learn_url(url: str) -> str:
517    """Fetch any URL, extract clean content, analyze with Claude...
518    Args:
519        url: Full URL to fetch and analyze (http or https)
520    """
521    return await _joao_learn_url(url)
mcp_server.py:515 -> tools/learning.py

// Exploitable by any client that can connect to the MCP endpoint.

EXPLAINThe joao_learn_url tool accepts an arbitrary URL and fetches it from the server. There is no validation that the URL is an allowed domain or protocol. An attacker could use this to perform Server-Side Request Forgery (SSRF) attacks, accessing internal services (e.g., http://localhost:8100, http://192.168.0.55) or cloud metadata endpoints (e.g., http://169.254.169.254). The fetched content is then sent to Claude for analysis, potentially leaking internal service responses.
IMPACTAn attacker can probe internal network services, access cloud metadata (e.g., AWS IAM credentials), or scan internal ports. The response content may be returned to the attacker via the tool output.
FIXRestrict allowed URLs to a whitelist of domains (e.g., *.youtube.com, *.github.com) or implement a blocklist for internal IP ranges. Validate the URL scheme (only http/https) and resolve the hostname to ensure it does not point to a private IP.
HIGH1 finding
mcp_server.py:245
245@mcp.tool()
246async def ftp_access(
247    action: str,
248    host: str,
249    remote_path: str,
250    user: str,
251    password: str,
252    port: int = 21,
253    local_path: str = "",
254) -> str:
255    """Access files on a remote FTP server — list, download, upload, or delete."""
mcp_server.py:245 -> services/ftp_client.py

// Exploitable by any client that can connect to the MCP endpoint. Credentials are also logged in Supabase.

EXPLAINThe ftp_access tool requires the caller to provide FTP username and password as plaintext arguments. These credentials are transmitted over the MCP transport (SSE or Streamable HTTP) and logged in Supabase session logs. The credentials are also stored in memory on the server. This exposes FTP credentials to anyone who can intercept the MCP traffic or access the logs.
IMPACTAn attacker who intercepts MCP traffic or gains access to Supabase logs can obtain FTP credentials, allowing unauthorized access to external FTP servers.
FIXStore FTP credentials in a secure vault (e.g., environment variables or a secrets manager) and reference them by alias. Do not accept credentials as tool arguments. Use SFTP/SCP instead of plain FTP.
HIGH1 finding
mcp_server.py:52
52@mcp.tool()
53async def dispatch_agent(session_name: str, command: str, wait: bool = False) -> str:
54    """Dispatch a shell command to a tmux session on the home server."""
55    ...
56    result = await dispatch.dispatch_raw_to_agent(session_name, command)
57    ...
mcp_server.py:56 -> services/dispatch.py

// Exploitable by any client that can connect to the MCP endpoint (SSE or Streamable HTTP). The server is network_exposed via Cloudflare tunnel.

EXPLAINThe dispatch_agent tool accepts a 'command' string and sends it directly to a tmux session on the home server. The command is executed as a shell command with no validation beyond agent name allowlisting. The tool's documented purpose is to dispatch shell commands, but the scope is unbounded — any shell command can be executed, including destructive or exfiltration commands. The tool is exposed via MCP SSE and Streamable HTTP, and the threat model is network_exposed, meaning any client that can connect to the MCP endpoint can invoke this tool.
IMPACTAn attacker (or compromised LLM) can execute arbitrary shell commands on the home server, including reading sensitive files, installing malware, exfiltrating data, or pivoting to internal network resources.
FIXRestrict the tool to a predefined list of allowed commands or use a sandboxed execution environment. Validate the command against a whitelist of safe operations. Alternatively, require additional authentication for this tool beyond MCP transport security.
HIGH1 finding
mcp_server.py:489
489@mcp.tool()
490async def joao_learn_pdf(file_path: str) -> str:
491    """Extract a PDF, generate an HTML intelligence report...
492    Args:
493        file_path: Absolute path to the .pdf file on the server
494    """
495    return await _joao_learn_pdf(file_path)
496
497@mcp.tool()
498async def joao_learn_excel(file_path: str) -> str:
499    ...
500    Args:
501        file_path: Absolute path to the .xlsx, .xls, or .csv file on the server
502    """
503    return await _joao_learn_excel(file_path)
504
505@mcp.tool()
506async def joao_learn_docx(file_path: str) -> str:
507    ...
508    Args:
509        file_path: Absolute path to the .docx file on the server
510    """
511    return await _joao_learn_docx(file_path)
mcp_server.py:489 -> tools/learning.py

// Exploitable by any client that can connect to the MCP endpoint. The server is network_exposed.

EXPLAINThese tools accept an arbitrary absolute file path and read the file from the server filesystem. The documented purpose is to process PDFs, Excel files, and Word docs for learning, but there is no validation that the path is within an allowed directory. An attacker could read any file on the server, including /etc/passwd, SSH keys, or environment files. The file content is then sent to Claude for analysis, and the result (including extracted content) is returned to the caller.
IMPACTAn attacker can read arbitrary files from the server filesystem, leading to credential theft, source code disclosure, or exposure of sensitive data.
FIXRestrict file paths to a specific allowed directory (e.g., /home/zamoritacr/joao/outputs/) and validate that the resolved path is within that directory. Use path canonicalization to prevent traversal attacks.
HIGH1 finding
lib/hub_tools/handlers.py:294
294def _resolve_memory_path(file_key: str) -> Path:
295    if file_key == "master":
296        return _MASTER_FILE
297    return _SESSION_FILE
298
299async def _h_joao_memory(args: dict[str, Any]) -> dict[str, Any]:
300    ...
301    file_key = args.get("file") or "session"
302    if file_key not in ("master", "session"):
303        raise ValueError("file must be 'master' or 'session'")
304    path = _resolve_memory_path(file_key)
305    ...
306    with path.open("a", encoding="utf-8") as fh:
307        fh.write(appendix + "\n")
mcp_server.py:457 -> tools/memory.py

// Exploitable by any client that can connect to the MCP endpoint.

EXPLAINThe joao_memory_write tool (and its hub_tools counterpart) validates the 'file' parameter against a fixed set ('master', 'session'), so direct path traversal is blocked. However, the underlying implementation in tools/memory.py may accept arbitrary file paths. Additionally, the joao_learn_* tools write output files to /joao/outputs/ with no path validation, allowing an attacker to control the output path and overwrite arbitrary files.
IMPACTAn attacker could overwrite critical system files or configuration files, leading to denial of service or code execution.
MEDIUM1 finding
mcp_server.py:477
477@mcp.tool()
478async def joao_learn_youtube(url: str) -> str:
479    """Extract a YouTube transcript, analyze with Claude...
480    Args:
481        url: Full YouTube URL (youtube.com or youtu.be)
482    """
483    return await _joao_learn_youtube(url)
mcp_server.py:477 -> tools/learning.py

// Exploitable by any client that can connect to the MCP endpoint.

EXPLAINThe joao_learn_youtube tool accepts a URL but does not validate that it is a legitimate YouTube URL. An attacker could provide a URL pointing to an internal service or a malicious site, potentially triggering SSRF or fetching unintended content.
IMPACTAn attacker could use this tool to fetch arbitrary URLs, similar to joao_learn_url, but with the pretense of being a YouTube-specific tool.
FIXValidate that the URL matches youtube.com or youtu.be domains. Use a URL parser to extract the video ID and reject invalid formats.
MEDIUM1 finding
mcp_server.py:132
132@mcp.tool()
133async def capture_idea(text: str, source: str = "mcp", context: str = "") -> str:
134    """Process text through AI and save to idea vault..."""
135    t0 = time.time()
136    ai_result = await ai_processor.process_text(text, context)
137    ...
138    notify_msg = f"*{ai_result.title}*\n{ai_result.summary}\nTags: {', '.join(ai_result.tags)}"
139    await telegram.send_notification(notify_msg)
mcp_server.py:132 -> services/ai_processor.py

// Exploitable by any client that can connect to the MCP endpoint. The impact depends on the capabilities granted to the AI model.

EXPLAINTools like capture_idea, joao_chat, and the joao_learn_* tools accept user-provided text that is passed to AI models (Claude) for processing. While this is the intended purpose, the text is not sanitized for prompt injection. An attacker could craft input that manipulates the AI model's behavior, potentially causing it to leak information or execute unintended actions. Additionally, the output of AI processing is sent to Telegram and stored in Supabase, which could be used as an exfiltration channel.
IMPACTAn attacker could perform prompt injection attacks to manipulate the AI model, potentially causing it to reveal sensitive information from the system prompt or execute unintended tool calls.
FIXImplement input sanitization to strip or escape control characters and known prompt injection patterns. Use a separate, sandboxed model for processing untrusted input. Limit the context available to the model when processing user input.
MEDIUM1 finding
mcp_server.py:245
245elif action == "delete":
246    result_text = await ftp_client.ftp_delete(host, port, user, password, remote_path)
mcp_server.py:245 -> services/ftp_client.py

// Exploitable by any client that can connect to the MCP endpoint, provided they have FTP credentials.

EXPLAINThe ftp_access tool supports a 'delete' action that can delete files on a remote FTP server. The documented purpose is to 'access files' (list, download, upload, or delete), but the delete capability is destructive and could be used to delete critical files on external systems. Combined with the credential exposure issue, this is a significant risk.
IMPACTAn attacker with valid FTP credentials could delete files on external FTP servers, causing data loss or service disruption.
FIXRemove the delete action or require explicit confirmation. Restrict delete to specific directories or file patterns.
MEDIUM1 finding
mcp_server.py:83
83except (httpx.ConnectError, httpx.ConnectTimeout) as tunnel_err:
84    # Tunnel unreachable -- try SSH as local-network fallback (with timeout)
85    logger.warning("dispatch_agent tunnel unreachable, trying SSH: %s", tunnel_err)
86    try:
87        ssh_result = await asyncio.wait_for(
88            dispatch.dispatch_command(session_name, command, wait),
89            timeout=15.0,
90        )
mcp_server.py:83 -> services/dispatch.py

// Exploitable if the attacker can cause the tunnel to fail (e.g., by network manipulation) or if the MCP server is compromised.

EXPLAINWhen the HTTP tunnel to the home server is unreachable, dispatch_agent falls back to SSH to execute the command directly on the internal network. This expands the attack surface: if the MCP server is compromised, an attacker could use this fallback to execute commands on the internal network via SSH, bypassing the tunnel authentication.
IMPACTAn attacker who can trigger the fallback (e.g., by blocking the tunnel) could execute commands on the internal home server via SSH, potentially gaining persistent access to the internal network.
FIXRemove the SSH fallback or require additional authentication for SSH-based dispatch. Alternatively, make the fallback opt-in with explicit configuration.
MEDIUM1 finding
mcp_server.py:198
198@mcp.tool()
199async def scout_intel(limit: int = 20, min_score: int = 7) -> str:
200    ...
201    items = scout_service.get_recent_intel(limit=limit, min_score=min_score)
202
203@mcp.tool()
204async def query_memory(query: str, limit: int = 10) -> str:
205    ...
206    results = await supabase_client.query_memory(query, limit)
mcp_server.py:198 -> services/scout.pymcp_server.py:218 -> services/supabase_client.py

// Exploitable by any client that can connect to the MCP endpoint.

EXPLAINThe scout_intel and query_memory tools accept integer parameters (limit, min_score) without validating that they are within reasonable bounds. While the code does clamp values in some places (e.g., max(1, min(50, int(args.get('limit', 10)))) in handlers.py), the MCP tool definitions do not enforce these limits. An attacker could pass extremely large values to cause resource exhaustion or denial of service.
IMPACTAn attacker could cause excessive database queries or memory consumption by passing large limit values, potentially leading to denial of service.
FIXAdd input validation in the tool functions to clamp limit and min_score to reasonable ranges (e.g., 1-100 for limit, 0-10 for min_score).
LOW1 finding
main.py:294
294@app.get("/joao/terminal", include_in_schema=False)
295async def terminal_ui():
296    token = os.environ.get("JOAO_TERMINAL_TOKEN") or os.environ.get("JOAO_DISPATCH_HMAC_SECRET", "")
297    html = (_STATIC_DIR / "terminal.html").read_text()
298    html = html.replace("__TERMINAL_TOKEN__", token)
299    return HTMLResponse(html)
main.py:294

// Exploitable by any client that can reach the server. The endpoint is not in the OpenAPI schema but is accessible via direct URL.

EXPLAINThe terminal UI endpoint embeds the JOAO_TERMINAL_TOKEN (or fallback HMAC secret) directly into the HTML response. This token is sent to every client that requests /joao/terminal, including unauthorized users. The token is used for WebSocket authentication and could be used to gain unauthorized access to the terminal.
IMPACTAn attacker who can access the /joao/terminal endpoint can obtain the terminal token and potentially connect to the terminal WebSocket, gaining shell access to the server.
FIXDo not embed secrets in HTML. Instead, use a session-based authentication mechanism or require the user to provide a token via a login flow. If embedding is necessary, ensure the endpoint is protected by authentication.
LOW1 finding
mcp_server.py:308
308@mcp.tool()
309async def council_dispatch(
310    agent: str,
311    task: str,
312    priority: str = "normal",
313    context: str = "",
314    project: str = "",
315) -> str:
316    ...
317    result = await dispatch.dispatch_to_agent(
318        agent=agent,
319        task=task,
320        ...
321    )
mcp_server.py:308 -> services/dispatch.py

// Exploitable by any client that can connect to the MCP endpoint.

EXPLAINThe council_dispatch tool accepts an 'agent' parameter but does not validate it against the allowed list of agents before passing it to the dispatch service. The dispatch service itself may validate, but the MCP tool does not provide early validation. An attacker could pass an invalid agent name, potentially causing errors or unexpected behavior.
IMPACTAn attacker could cause errors or potentially bypass agent restrictions if the dispatch service has inconsistent validation.
FIXAdd validation of the agent name against the allowed list in the MCP tool function before calling the dispatch service.
6/11/2026
Findings are produced by automated LLM analysis and may include false positives or miss issues. Verify independently before acting.