From b31736cde79a872a494b930b9e30c4de2a869f9c Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Mon, 29 Jun 2026 16:46:41 +0200 Subject: [PATCH] =?UTF-8?q?Sec:=20PC-Executor=20Bearer-Token-Pflicht=20(RC?= =?UTF-8?q?E-L=C3=BCcke=20schlie=C3=9Fen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit executor.py erzwingt jetzt HERMES_PC_TOKEN auf allen Steuer-Endpunkten (/shell,/screenshot,/type,/key,/open,/search), fail-closed (503) wenn kein Token gesetzt ist; /health bleibt offen für den Reachability-Check. CORS- Wildcard entfernt, Bind-Host konfigurierbar (HERMES_PC_HOST). mcp_pc.py sendet PC_EXECUTOR_TOKEN als Authorization-Bearer mit. Schließt die unauthentifizierte Remote-Code-Execution auf dem Windows-PC (host=0.0.0.0, kein Token) — Teil von Stufe 0 (Security) des Stack-Reviews. Co-Authored-By: Claude Opus 4.8 --- client/hermes-pc/executor.py | 44 +++++++++++++++++++++++++++--------- mcp/mcp_pc.py | 7 ++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/client/hermes-pc/executor.py b/client/hermes-pc/executor.py index d402843..948d1cd 100644 --- a/client/hermes-pc/executor.py +++ b/client/hermes-pc/executor.py @@ -3,6 +3,7 @@ Hermes PC Executor — läuft auf dem Windows PC. Empfängt Tool-Befehle vom MCP-Server der AI Box und führt sie lokal aus. """ import base64 +import hmac import io import os import socket @@ -11,18 +12,38 @@ import webbrowser from urllib.parse import quote import uvicorn -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware +from fastapi import Depends, FastAPI, Header, HTTPException app = FastAPI(title="Hermes PC Executor") -app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) MY_PORT = int(os.environ.get("HERMES_PC_PORT", "7777")) +# Auf welchem Interface lauschen. Default 0.0.0.0 (LAN), per Env einschränkbar (z.B. die LAN-IP des PCs). +MY_HOST = os.environ.get("HERMES_PC_HOST", "0.0.0.0") + +# ── Auth ─────────────────────────────────────────────────────────────────── +# Shared Secret. OHNE Token sind die gefährlichen Endpunkte (Shell/Eingabe/Öffnen/Screenshot) +# fail-closed gesperrt → ein un-konfigurierter Executor ist KEINE offene Remote-Code-Execution mehr. +# Der Token muss identisch auf der Box (Hermes-Env PC_EXECUTOR_TOKEN → mcp_pc.py) gesetzt sein. +AUTH_TOKEN = os.environ.get("HERMES_PC_TOKEN", "").strip() + + +def require_auth(authorization: str | None = Header(default=None)) -> None: + """Bearer-Token-Prüfung (konstante Zeit). 503 wenn der Executor ohne Token läuft (fail-closed), + 401 bei fehlendem/falschem Token.""" + if not AUTH_TOKEN: + raise HTTPException( + 503, + "Executor ohne HERMES_PC_TOKEN gestartet — Steuer-Endpunkte sind aus Sicherheitsgründen " + "gesperrt. Setze die Env-Variable HERMES_PC_TOKEN (identisch zur Box) und starte neu.", + ) + expected = f"Bearer {AUTH_TOKEN}" + if not authorization or not hmac.compare_digest(authorization, expected): + raise HTTPException(401, "Ungültiges oder fehlendes Bearer-Token.") # ── Shell ────────────────────────────────────────────────────────────────── -@app.post("/shell") +@app.post("/shell", dependencies=[Depends(require_auth)]) async def run_shell(req: dict): cmd = req.get("command", "") try: @@ -44,7 +65,7 @@ async def run_shell(req: dict): # ── Screen ───────────────────────────────────────────────────────────────── -@app.post("/screenshot") +@app.post("/screenshot", dependencies=[Depends(require_auth)]) async def take_screenshot(): try: import mss @@ -64,7 +85,7 @@ async def take_screenshot(): # ── Input ────────────────────────────────────────────────────────────────── -@app.post("/type") +@app.post("/type", dependencies=[Depends(require_auth)]) async def type_text(req: dict): text = req.get("text", "") try: @@ -75,7 +96,7 @@ async def type_text(req: dict): raise HTTPException(500, str(e)) -@app.post("/key") +@app.post("/key", dependencies=[Depends(require_auth)]) async def press_keys(req: dict): keys = req.get("keys", "") try: @@ -89,7 +110,7 @@ async def press_keys(req: dict): # ── Apps / Browser ───────────────────────────────────────────────────────── -@app.post("/open") +@app.post("/open", dependencies=[Depends(require_auth)]) async def open_target(req: dict): target = req.get("target", "") try: @@ -102,7 +123,7 @@ async def open_target(req: dict): raise HTTPException(500, str(e)) -@app.post("/search") +@app.post("/search", dependencies=[Depends(require_auth)]) async def search_web(req: dict): query = req.get("query", "") url = f"https://www.google.com/search?q={quote(query)}" @@ -145,5 +166,6 @@ def _ensure_streams() -> None: if __name__ == "__main__": _ensure_streams() my_ip = _get_local_ip() - print(f"Hermes PC Executor läuft auf http://{my_ip}:{MY_PORT}") - uvicorn.run(app, host="0.0.0.0", port=MY_PORT) + auth_state = "Token AKTIV" if AUTH_TOKEN else "KEIN Token → Steuer-Endpunkte GESPERRT (fail-closed)" + print(f"Hermes PC Executor läuft auf http://{my_ip}:{MY_PORT} (bind {MY_HOST}) — {auth_state}") + uvicorn.run(app, host=MY_HOST, port=MY_PORT) diff --git a/mcp/mcp_pc.py b/mcp/mcp_pc.py index ebe0f9e..ce2d986 100644 --- a/mcp/mcp_pc.py +++ b/mcp/mcp_pc.py @@ -14,6 +14,8 @@ import httpx from mcp.server.fastmcp import FastMCP PC_URL = os.environ.get("PC_EXECUTOR_URL", "").rstrip("/") +# Shared Secret — muss identisch zum HERMES_PC_TOKEN des Executors auf dem Windows-PC sein. +PC_TOKEN = os.environ.get("PC_EXECUTOR_TOKEN", "").strip() mcp = FastMCP("hermes-pc-control") @@ -21,12 +23,13 @@ mcp = FastMCP("hermes-pc-control") def _pc(path: str, data: dict | None = None) -> dict: if not PC_URL: return {"error": "PC_EXECUTOR_URL nicht gesetzt. Setze die Env-Variable mit der IP des Windows PCs."} + headers = {"Authorization": f"Bearer {PC_TOKEN}"} if PC_TOKEN else {} try: with httpx.Client(timeout=90) as c: if data is None: - r = c.get(f"{PC_URL}{path}") + r = c.get(f"{PC_URL}{path}", headers=headers) else: - r = c.post(f"{PC_URL}{path}", json=data) + r = c.post(f"{PC_URL}{path}", json=data, headers=headers) r.raise_for_status() return r.json() except httpx.ConnectError: