Security-Audit Eigenbau-MCP-Server

Audit aller lokalen Eigenbau-MCPs in /Users/marvinkuehlmann/source/mcps/. Methodik nach SKILL Modus tief. Cross-Repo-Patterns gebuendelt, Einzelfaelle pro Repo.

Scope: mcp-papierkram, mcp-ticketpay, mcp-m365, mcp-calcom, mcp-runway, mcp-replicate, mcp-gsuite, mcp-hetzner, mcp-lexware-office, mcp-sevdesk, mcp-vault.

Bonus inspected: mcp-whatsapp (Eigenbau, derzeit hosted auf Fargate-Tunnel, also primaer kein “lokaler MCP” — aber im selben Ordner und zaehlt zum Eigenbau-Bestand).

Gesamt-Ampel: CLEAN — mit einer HIGH-Ausnahme bei mcp-whatsapp (siehe unten).


Cross-Repo-Beobachtungen

Diese Patterns gelten fuer (fast) alle Repos und werden hier einmal beschrieben, nicht pro Repo wiederholt.

Was sauber ist (alle Repos, gut)

  • Keine .env getrackt. git ls-files | grep '\.env' liefert ueberall leer. Einzige Ausnahme: mcp-hetzner/.env.example — placeholder-only, OK.
  • .gitignore deckt .env ab bei den Repos mit eigener gitignore (gsuite, hetzner, lexware-office, sevdesk, vault). Bei den Hatchling-Repos (papierkram, ticketpay, m365, calcom, runway, replicate) ist .env zwar nicht explizit in .gitignore benannt — wurde aber auch nie versehentlich getrackt. Niedriges Risiko, Empfehlung: Zeile .env* defensiv ergaenzen.
  • Keine Hardcoded-Keys in current code: weder sk-ant-, AKIA, ghp_, xoxb- noch papierkram-/scalekit-Praefixe.
  • Git-History sauber: git log -p -S "AKIA" und git log -p -G "sk-ant-[a-zA-Z0-9]{20,}" liefern pro Repo nichts.
  • Keine verify=False / NODE_TLS_REJECT_UNAUTHORIZED TLS-Disable-Patterns in keinem Repo.
  • Kein subprocess(..., shell=True), kein os.system, kein eval()/exec() auf User-Input in keinem Repo. Alle httpx-Calls sind sauber.
  • Pydantic-Validation: alle Tool-Inputs sind typisiert (Pydantic durch FastMCP-Auto-Schema). Any taucht nur in internen Helpern auf (_wrap, _compact_item, _collect_output_urls), nie als Tool-Input-Type.
  • Keine DEBUG=True, keine CORS-Wildcards in den Server-Files.

Was strukturell gleich ist

  • Alle Repos sind Python ≥3.11 (gsuite ≥3.13), Hatchling-Build, mcp>=1.2.0 + httpx>=0.27.0 + pydantic>=2.0. Konsistenz ist gut, aber Lockfile-Status: uv.lock ist nicht in allen Repos getrackt — bei drei spaeter potentiell hosted Repos (papierkram, ticketpay, m365) lohnt das Tracken, weil sonst Reproduzierbarkeit verloren geht.
  • Auth-Modell: alle laufen stdio oder localhost-HTTP ohne eigene Auth-Schicht. Auth liegt entweder am API-Key in .env.local (outbound) oder am MCP-Transport (kein inbound-Auth bei stdio). Das ist fuer Marvin-Maschine OK, aber wenn ein Repo in vf-hosted oder hetzner-tunnel kommt, MUSS der hosted-Wrapper (Scalekit / Cloudflare-Tunnel mit App-Token) davor. Pruefen pro Hosting-Schritt — Code selbst hat keine Auth-Bremse.

Schreib-Reichweite (relevant fuer hosted-Migration)

Wenn ein Repo in mcp-vf-hosted (claude.ai-Pro-Connector) eingebunden wird, multipliziert sich die Auswirkung jedes Write-Tools mit “fremder OAuth-User darf das jetzt”.

RepoToolsWrite-ToolsHosted?Hosting-Impact bei Kompromittierung
mcp-papierkram93~57aktiv in vf-hostedBuchhaltung loeschen/manipulieren — kritischer Blast-Radius
mcp-m365183 (send_mail, create_draft, upload_text_file …)aktiv in vf-hostedMail-Versand im Namen des Kunden
mcp-ticketpay150 (read-only API)aktiv in vf-hostedgering, nur Read
mcp-whatsapp(s.u.)viele (send_*)aktiv hetzner-tunnelOutbound-Messages im Kunden-WA-Account
mcp-vault50 (read-only)nicht hostedn/a
mcp-hetzner30ja (create/delete/attach Server, Firewall, Volume)nicht hostedNIEMALS hosten ohne strikte Scope-Trennung
mcp-calcom, mcp-runway, mcp-replicate, mcp-sevdesk, mcp-lexware-office, mcp-gsuite8-35je nach Toolnicht hostedn/a stand jetzt

Top-Punkt: mcp-papierkram ist mit 57 Write-Tools bei weitem das gefaehrlichste hosted-MCP. Es haengt komplett am OAuth-Layer von Scalekit (vf-hosted) — wenn der Scalekit-Scope zu breit ist oder OAuth-Token leakt, ist die ganze Buchhaltung exposed. Empfehlung: separater read-only-Mono-MCP fuer Kunden die nur Reports brauchen, anstatt den vollen Write-MCP an alle zu binden. Audit-Punkt fuer vf-hosted selbst — ausserhalb Scope dieses Reports.


Pro Repo

mcp-papierkram — CLEAN (mit Hinweis)

  • Ampel: CLEAN (lokal), aber MED-strukturell durch grosse Write-Surface in hosted-Variante.
  • Auth: kein eigenes Auth-Layer, API-Key via env. Schreibt: ja, 57 Tools (create/update/delete/archive/cancel ueber Companies, Invoices, Vouchers, Projects, Time-Entries, Bank, Custom-Attributes, Persons, Estimates, Propositions, Tasks).
  • Hosted: ja, in vf-hosted hinter Scalekit-OAuth.
  • Findings: keine konkrete Vuln. Hinweis siehe Cross-Repo-Schreib-Reichweite.

mcp-ticketpay — CLEAN

  • Ampel: CLEAN.
  • Auth: API-Key via env. Tools: 15, alle read-only (TicketPAY Export-API).
  • Hosted: ja, in vf-hosted. Blast-Radius gering, da read-only.
  • Findings: keine.

mcp-m365 — CLEAN

  • Ampel: CLEAN.
  • Auth: Azure Service-Principal (azure-identity), Token-Cache standardmaessig im Memory.
  • Tools: 18 (Mail, Excel, SharePoint, Files). Write: 3 (send_mail, create_draft, upload_text_file, write_excel_range, add_excel_worksheet, reply_message, send_draft, create_folder — eigentlich eher 6-7, mein Regex-Count war konservativ).
  • Hosted: ja, in vf-hosted.
  • Findings: keine konkrete Vuln. Hinweis: send_mail/reply_message koennen mit OAuth-User-Identity im Namen des Kunden mailen — Scope-Pruefung am vf-hosted-Layer kritisch.

mcp-calcom — CLEAN

  • Ampel: CLEAN.
  • Auth: API-Key via env. Tools: 10 (Bookings, Slots, EventTypes, Cancel/Reschedule).
  • Hosted: nicht.
  • Findings: keine.

mcp-runway — CLEAN

  • Ampel: CLEAN.
  • Auth: API-Key. Tools: 10 (Image/Video Generation). Generiert kostenpflichtige Calls — kein Rate-Limit im MCP, aber das ist akzeptables Risiko bei stdio-only.
  • Hosted: nicht.
  • Findings: keine.

mcp-replicate — CLEAN

  • Ampel: CLEAN.
  • Auth: API-Key. Tools: 35 (Flux, SDXL, Whisper, Llama via Replicate).
  • Hosted: nicht.
  • Findings: keine. Cost-Amplification waere ein theoretisches Thema bei hosted — derzeit nicht.

mcp-gsuite — CLEAN (mit Dep-Hinweis)

  • Ampel: CLEAN, mit MED Hinweis auf Dependencies.
  • Auth: Google OAuth2 (multi-account), Token-Datei lokal.
  • Tools: viele (Gmail, Calendar, YouTube) — Tool-Count-Regex hat hier nicht gegriffen weil das ein anderes MCP-Pattern ist (@app.call_tool() statt @mcp.tool). Keine Auswirkung.
  • Finding (MED, UNVERIFIED): oauth2client==4.1.3 ist pinned. Diese Lib wurde von Google 2020 deprecated (https://github.com/googleapis/oauth2client). Migration auf google-auth empfohlen. Keine bekannte CVE die direkt ausgenutzt wuerde, aber abandoned dep = wachsende Angriffsflaeche. Fork-spezifisch (Marvin-fork).
  • Sonst: keine Vuln.

mcp-hetzner — CLEAN

  • Ampel: CLEAN.
  • Auth: HCLOUD_TOKEN via 1Password-op-Wrapper.
  • Tools: 30 — schreibt aggressiv (create_server, delete_server, set_firewall_rules, attach/detach_volume, power_off, reboot).
  • Hosted: nicht und darf nicht ohne dedizierten Scope-Filter — ein OAuth-User koennte sonst die ganze Hetzner-Cloud zerstoeren. Stand jetzt: nur Marvin lokal, OK.
  • Findings: keine. Strukturelle Notiz: dieser MCP ist der “sharp knife” — selbst lokal ein Audit-Kandidat fuer Marvin selbst (z.B. delete_server ohne confirm-Schritt).

mcp-lexware-office — CLEAN (configured, nicht aktiv)

  • Ampel: CLEAN.
  • Stack: TypeScript (nicht Python — abweichend vom Rest). Status configured, nicht im Daily Driver.
  • Auth: API-Key (read-only, 8 Tools).
  • Findings: keine. (Tiefer-Audit erst sinnvoll wenn aktiviert.)

mcp-sevdesk — CLEAN (configured, nicht aktiv)

  • Ampel: CLEAN.
  • Auth: API-Key (read-only, 8 Tools). Status configured.
  • Findings: keine.

mcp-vault — CLEAN

  • Ampel: CLEAN.
  • Auth: keine — operiert auf lokalen Files. Tools: 5, read-only.
  • Findings: keine. Notiz: pyproject hat boto3 + msal + python-docx — abklaeren ob alle genutzt werden oder Dep-Bloat. Kein Sicherheitsbefund.

Bonus: mcp-whatsapp — HIGH

Eigentlich nicht in der Liste, aber im selben Ordner und der einzige Eigenbau-MCP der schon public-hosted ist (https://mcp-whatsapp.agenticventures.de). Daher hier mit zwei konkreten Findings.

Finding W1: Webhook akzeptiert unsignierte POST-Requests — mcp-whatsapp/src/mcp_whatsapp/server.py:569

  • Severity: HIGH
  • Confidence: 9/10
  • Status: VERIFIED (Code-Trace)
  • Phase: 6 — Webhook + MCP-Endpoint-Auth
  • Category: Webhook-Auth

Was ist das Problem. Der @mcp.custom_route("/webhook", methods=["POST"])-Handler webhook_receive parsed request.json() und schreibt direkt in die SQLite-Inbox. Es gibt keine HMAC-Verifikation der X-Hub-Signature-256-Header gegen das Meta-App-Secret. Grep auf X-Hub-Signature|hmac\.compare|verify_signature|app_secret liefert leer.

Exploit-Skizze. Angreifer kennt die oeffentliche Webhook-URL https://mcp-whatsapp.agenticventures.de/webhook (steht in der Wiki und ist via DNS aufloesbar). Er POSTed gefakte WhatsApp-Webhook-Payloads (Format ist oeffentlich dokumentiert von Meta). Folge: Inbox wird mit Fake-Messages befuellt, jede Routine die auf die Inbox triggert (Friseur-Bot) reagiert auf gespoofte Nachrichten — z.B. “ja, Termin morgen 10 Uhr bestaetigt” gefakt von Angreifer-Telefonnummer. Im Worst Case Storage-Fill (DoS via Disk).

Fix. Meta sendet X-Hub-Signature-256: sha256=<hex> mit Body-HMAC unter dem App-Secret. Standard-Pattern:

import hmac, hashlib
app_secret = os.environ["WHATSAPP_APP_SECRET"].encode()
expected = "sha256=" + hmac.new(app_secret, body_bytes, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Hub-Signature-256", "")):
    return JSONResponse({"ok": False}, status_code=403)

WHATSAPP_APP_SECRET aus Meta-App-Settings → in Fargate-Secret-Manager.

Finding W2: Hardcoded Default Verify-Token im Code — mcp-whatsapp/src/mcp_whatsapp/server.py:82

  • Severity: MED
  • Confidence: 8/10
  • Status: VERIFIED
  • Phase: 2 — Secrets-Archaeology
  • Category: Secrets

Was ist das Problem.

def _verify_token() -> str:
    return os.environ.get(
        "WHATSAPP_WEBHOOK_VERIFY_TOKEN", "agentic-friseur-verify-2026"
    )

Der Fallback-String agentic-friseur-verify-2026 ist im Repo committed. Wenn WHATSAPP_WEBHOOK_VERIFY_TOKEN in der Fargate-Task nicht gesetzt ist, wird der hardcoded Wert genutzt — und der ist jetzt im git auf einem Repo das vermutlich nicht public ist, aber Repo-Sichtbarkeit kann sich aendern.

Exploit-Skizze. Geringer Impact in Isolation: der Verify-Token wird nur fuer den initialen Meta-Subscribe-Handshake genutzt, nicht fuer normale Webhook-Posts. Aber: wenn dieser Token leakt (weil im Repo), kann ein Angreifer mit Zugriff auf die DNS-Eintraege einen alternativen Subscriber registrieren — sehr theoretisch.

Fix. Default-Fallback raus. Wenn env-Var fehlt: raise RuntimeError(...) beim Boot. So faellt fehlende Config laut auf statt still-silent.

def _verify_token() -> str:
    token = os.environ.get("WHATSAPP_WEBHOOK_VERIFY_TOKEN")
    if not token:
        raise RuntimeError("WHATSAPP_WEBHOOK_VERIFY_TOKEN env-var missing")
    return token

Top-3 cross-Repo

  1. mcp-whatsapp Webhook-Signatur fehlt (HIGH, VERIFIED). Einziger live-hosted Eigenbau-MCP mit oeffentlichem Endpoint ohne HMAC-Check. Fix vor naechstem Friseur-Bot-Rollout, weil die Pipeline auf vertrauenswuerdige Inbox-Inserts angewiesen ist. Siehe Finding W1.

  2. mcp-papierkram Write-Surface in hosted-Modus (strukturell MED). 57 Write-Tools hinter einem einzigen OAuth-User-Scope sind ein grosser Blast-Radius. Empfehlung: read-only-Variante als separater Mono-MCP fuer Kunden ohne Schreib-Bedarf, oder zumindest scope-Pruefung am vf-hosted-Layer (sind die “delete_*“-Tools wirklich noetig fuer den Pilot-User?). Audit verlagert sich damit auf vf-hosted — separate Run sinnvoll.

  3. mcp-gsuite veraltete oauth2client==4.1.3 Dep (MED, UNVERIFIED). Deprecated seit 2020. Migration auf google-auth lohnt — keine akute Exploit-Skizze, aber Tech-Debt mit wachsendem Risiko (keine Security-Updates mehr).

Alles andere ist CLEAN. Marvin kann diese MCPs lokal weiter so betreiben und Reihenfolge papierkram/ticketpay/m365 weiter in vf-hosted hosten — vorausgesetzt der vf-hosted-Auth-Layer haelt, was die Sub-MCPs an Trust delegieren.