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:
Hitonabi
2026-06-29 16:46:41 +02:00
parent 50e04e0aee
commit b31736cde7
2 changed files with 38 additions and 13 deletions
+33 -11
View File
@@ -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)