Sec: PC-Executor Bearer-Token-Pflicht (RCE-Lücke schließen)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
Empfängt Tool-Befehle vom MCP-Server der AI Box und führt sie lokal aus.
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
|
import hmac
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
@@ -11,18 +12,38 @@ import webbrowser
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import Depends, FastAPI, Header, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
app = FastAPI(title="Hermes PC Executor")
|
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"))
|
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 ──────────────────────────────────────────────────────────────────
|
# ── Shell ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/shell")
|
@app.post("/shell", dependencies=[Depends(require_auth)])
|
||||||
async def run_shell(req: dict):
|
async def run_shell(req: dict):
|
||||||
cmd = req.get("command", "")
|
cmd = req.get("command", "")
|
||||||
try:
|
try:
|
||||||
@@ -44,7 +65,7 @@ async def run_shell(req: dict):
|
|||||||
|
|
||||||
# ── Screen ─────────────────────────────────────────────────────────────────
|
# ── Screen ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/screenshot")
|
@app.post("/screenshot", dependencies=[Depends(require_auth)])
|
||||||
async def take_screenshot():
|
async def take_screenshot():
|
||||||
try:
|
try:
|
||||||
import mss
|
import mss
|
||||||
@@ -64,7 +85,7 @@ async def take_screenshot():
|
|||||||
|
|
||||||
# ── Input ──────────────────────────────────────────────────────────────────
|
# ── Input ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/type")
|
@app.post("/type", dependencies=[Depends(require_auth)])
|
||||||
async def type_text(req: dict):
|
async def type_text(req: dict):
|
||||||
text = req.get("text", "")
|
text = req.get("text", "")
|
||||||
try:
|
try:
|
||||||
@@ -75,7 +96,7 @@ async def type_text(req: dict):
|
|||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/key")
|
@app.post("/key", dependencies=[Depends(require_auth)])
|
||||||
async def press_keys(req: dict):
|
async def press_keys(req: dict):
|
||||||
keys = req.get("keys", "")
|
keys = req.get("keys", "")
|
||||||
try:
|
try:
|
||||||
@@ -89,7 +110,7 @@ async def press_keys(req: dict):
|
|||||||
|
|
||||||
# ── Apps / Browser ─────────────────────────────────────────────────────────
|
# ── Apps / Browser ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/open")
|
@app.post("/open", dependencies=[Depends(require_auth)])
|
||||||
async def open_target(req: dict):
|
async def open_target(req: dict):
|
||||||
target = req.get("target", "")
|
target = req.get("target", "")
|
||||||
try:
|
try:
|
||||||
@@ -102,7 +123,7 @@ async def open_target(req: dict):
|
|||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/search")
|
@app.post("/search", dependencies=[Depends(require_auth)])
|
||||||
async def search_web(req: dict):
|
async def search_web(req: dict):
|
||||||
query = req.get("query", "")
|
query = req.get("query", "")
|
||||||
url = f"https://www.google.com/search?q={quote(query)}"
|
url = f"https://www.google.com/search?q={quote(query)}"
|
||||||
@@ -145,5 +166,6 @@ def _ensure_streams() -> None:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
_ensure_streams()
|
_ensure_streams()
|
||||||
my_ip = _get_local_ip()
|
my_ip = _get_local_ip()
|
||||||
print(f"Hermes PC Executor läuft auf http://{my_ip}:{MY_PORT}")
|
auth_state = "Token AKTIV" if AUTH_TOKEN else "KEIN Token → Steuer-Endpunkte GESPERRT (fail-closed)"
|
||||||
uvicorn.run(app, host="0.0.0.0", port=MY_PORT)
|
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)
|
||||||
|
|||||||
+5
-2
@@ -14,6 +14,8 @@ import httpx
|
|||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
PC_URL = os.environ.get("PC_EXECUTOR_URL", "").rstrip("/")
|
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")
|
mcp = FastMCP("hermes-pc-control")
|
||||||
|
|
||||||
@@ -21,12 +23,13 @@ mcp = FastMCP("hermes-pc-control")
|
|||||||
def _pc(path: str, data: dict | None = None) -> dict:
|
def _pc(path: str, data: dict | None = None) -> dict:
|
||||||
if not PC_URL:
|
if not PC_URL:
|
||||||
return {"error": "PC_EXECUTOR_URL nicht gesetzt. Setze die Env-Variable mit der IP des Windows PCs."}
|
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:
|
try:
|
||||||
with httpx.Client(timeout=90) as c:
|
with httpx.Client(timeout=90) as c:
|
||||||
if data is None:
|
if data is None:
|
||||||
r = c.get(f"{PC_URL}{path}")
|
r = c.get(f"{PC_URL}{path}", headers=headers)
|
||||||
else:
|
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()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
except httpx.ConnectError:
|
except httpx.ConnectError:
|
||||||
|
|||||||
Reference in New Issue
Block a user