Raw Audit — mcp-papierkram

Daily schnell-Modus, Konfidenz-Gate 8/10. Erster Lauf im Projekt public-mcp-audits. Pilot. Quelle: ~/source/mcps/mcp-papierkram/ (mono-repo agentic-ventures/mcps, branch feat/360dialog-provider, HEAD 0ce2d60).

Surface

SURFACE: mcp-papierkram
- MCP-Tools: 96 (davon ~62 Write, ~34 Read inkl. PDFs)
- Lambda-Endpoints: 0
- Public-Endpoints: 0 (lokales Stdio + HTTP-Default 127.0.0.1:8767)
- Github-Actions: 0 (mono-repo hat keine workflows)
- Secrets-Stores: .env.local (gitignored), env-Variablen zur Laufzeit (PAPIERKRAM_TOKEN, PAPIERKRAM_SUBDOMAIN)
- Trust-Boundary: keine eigene Auth-Schicht; im hosted Setup haengt das MCP hinter Scalekit-OAuth in mcp-vf-hosted (FastMCP create_proxy + StdioTransport)

Stack: Python 3.11, FastMCP (mcp.server.fastmcp), httpx, pydantic. Single-file server.py (1569 Zeilen, 96 @mcp.tool-Functions + 4 Raw-Escape-Hatches + 3 Aggregations).

Mental Model

  • Was ist das? Lokaler MCP-Server fuer die Papierkram.de-Buchhaltungs-API. Wrapper um ~89 OpenAPI-Endpoints.
  • Wer schickt Input? Marvin via Claude Code (stdio) oder lokaler Claude Desktop. Im hosted Kontext (mcp-vf-hosted): Vibe-Factory-User via claude.ai Custom Connector hinter Scalekit-OAuth.
  • Wer hat Output-Zugriff? Selber Caller. Kein Logging an Dritte. PDFs werden base64-encoded zurueckgegeben.
  • Trust-Boundaries: Bearer-Token zur Papierkram-API (per Env), keine eigene Auth. In hosted Variante: Scalekit-JWT → MCP-Layer → Stdio-Subprocess → Papierkram.

Phasen-Findings

Phase 2 — Secrets-Archaeology

git log -p --all -G "PAPIERKRAM_TOKEN|sk-ant-|AKIA[A-Z0-9]{16}|ghp_|xoxb-" ueber den gesamten Mono-Repo: keine Treffer. .env.local ist in .gitignore (Eintrag .env.local). .env.local.example enthaelt nur Schluessel-Namen ohne Werte. mcp-papierkram/ selbst ist nur README.md, pyproject.toml, src/... und keine .env*-Files getrackt. Phase 2 clean.

Phase 3 — Dependency Supply Chain

uv tree aus Editable-Install zeigt:

  • httpx 0.28.1, mcp 1.27.1, pydantic 2.13.4, cryptography 48.0.0, h11 0.16.0, starlette 1.0.0, anyio 4.13.0.
  • h11 0.16.0 fixt CVE-2025-43859 (request-smuggling).
  • Keine bekannten unpatchten CVEs in den direkten Deps.

ABER: pyproject.toml pinnt nur untere Schranken (mcp>=1.2.0, httpx>=0.27.0, pydantic>=2.0). Keine uv.lock im Repo getrackt — zum Vergleich mcp-ticketpay/uv.lock und mcp-vf-hosted/uv.lock SIND getrackt. → siehe Finding F1.

Phase 4 — CI/CD

Keine .github/workflows/ im Mono-Repo. Kein Build-Pipeline-Risiko. railway.toml existiert auf Root-Ebene, ist aber Legacy (mcp-vf-hosted ist auf Fargate migriert). Nichts auditierbares.

Phase 5 — Infra Shadow Surface

Kein Dockerfile fuer mcp-papierkram/ selbst (das MCP wird via uv tool install --editable lokal oder als Sub-Subprocess in mcp-vf-hosted aufgerufen). Kein eigenes CDK, kein eigener Bucket. Out of scope fuer dieses Repo-Subtree.

Phase 6 — Endpoint-Auth

server.py:1561–1565 bindet bei streamable-http-Transport auf MCP_HTTP_HOST (default 127.0.0.1) und MCP_HTTP_PORT (default 8767). Keine eigene Auth-Schicht. → siehe Finding F2 (Raw-Tools) und F3 (Bind-Address).

httpx.Client wird mit Default-TLS-Verify benutzt — kein verify=False im Code. Bearer-Token wird in jedem _client()-Call frisch gelesen (kein Caching, kein Logging).

Phase 7 — LLM-Security

Tool-Descriptions sind statische Docstrings, kein f-String mit User-Input. Tool-Inputs sind ueberwiegend Pydantic-validated (FastMCP leitet Funktions-Signaturen automatisch in Schemas um). ABER: ~30 Create/Update-Tools nehmen data: dict als Free-Form-Parameter, ohne strikte Schema-Validation:

@mcp.tool()
def create_invoice(data: dict) -> dict:
    return _post("/income/invoices", data)

Konfidenz 6/10 fuer Prompt-Injection via Daten-zurueck-zu-Tool — unter dem 8/10-Gate. Notiert als TENTATIVE fuer tief-Modus.

Phase 8 — Skill + MCP Supply-Chain

Eigener MCP, eigene Quelle. Keine Skill-Files. Tool-Descriptions sauber, kein Network-Exfiltration-Pattern, keine curl ... | sh.

Phase 9 — OWASP-Mini

  • A01 Access Control: lokaler MCP, kein Multi-Tenant. Im hosted Setup wird papierkram_token und papierkram_subdomain per Settings-Class einmalig gelesen — alle Tool-Calls eines hosted MCP gehen gegen denselben Papierkram-Account. Korrekt fuer mcp-vf-hosted (Single-Tenant pro Deployment), waere problematisch wenn jemand das als Multi-Tenant deployen wollte.
  • A03 Injection: kein os.system, kein subprocess(shell=True), kein eval/exec, kein SQL. httpx-Requests bauen Pfade per f-String aus IDs (/contact/companies/{company_id}) — company_id ist int-typed in der Tool-Signatur, FastMCP validiert das via Pydantic.
  • A05 Misconfig: kein DEBUG-Flag, kein CORS-Wildcard.

Phase 10 — STRIDE

Nicht angewendet — Daily-Modus, kein Production-Multi-Tenant-Service.

Phase 11 — DSGVO

Lokaler MCP, keine eigene Persistenz. Papierkram-Daten (Buchhaltung, Kunden-IBAN, Rechnungen) sind RESTRICTED — der MCP gibt sie ungefiltert an den Caller (Claude Code) weiter. Das ist beabsichtigt und Teil des Threat-Models. Kein eigenes Logging der Tool-Inhalte (FastMCP-Default), kein CloudWatch.

Phase 12 — FP-Filter

Discarded:

  • Range-pinned Deps ohne uv.lock: behoben → F1 ist VERIFIED.
  • data: dict ohne Pydantic-Model: Konf 6/10 unter 8/10-Gate → TENTATIVE-Notiz.
  • Keine Size-Cap auf _pdf / upload_voucher_document: lokaler Scope, DoS ohne konkreten Exploit → P12 Hard-Exclusion #2.
  • _format_404_hint echoed path: keine Log-Injection oder XSS-Wirkung im MCP-Kontext → discard.

Findings

F1 — uv.lock fehlt + Dependencies range-pinned

Severity:    MED
Confidence:  8/10
Status:      VERIFIED
Phase:       3 — Dependency Supply Chain
Category:    Supply-Chain
File:        ~/source/mcps/mcp-papierkram/pyproject.toml
             ~/source/mcps/.gitignore

Was ist das Problem. pyproject.toml pinnt nur untere Schranken (mcp>=1.2.0, httpx>=0.27.0, pydantic>=2.0). Es gibt keinen mcp-papierkram/uv.lock im git-Tracking, obwohl mcp-ticketpay/uv.lock und mcp-vf-hosted/uv.lock getrackt sind. Bei jedem fresh-install / Docker-Build kann sich die transitive Versions-Topologie still aendern.

Exploit-Skizze. Ein kompromittiertes Mid-Stream-Release einer transitiven Dep (Beispiel-Vektor: anyio-Maintainer-Account-Takeover, oder python-multipart Supply-Chain-Attacke wie sie 2024 oeffentlich diskutiert wurden) wuerde beim naechsten Build automatisch eingezogen, ohne dass irgendwer es merkt. Real-World-Pendant: PyTorch-Nightly-Vorfall 2022 (torchtriton).

Fix.

cd ~/source/mcps/mcp-papierkram
uv lock
git add uv.lock

Plus: Im hosted Docker-Build (mcp-vf-hosted/Dockerfile) auf uv sync --frozen umstellen, damit die hosted Image-Build den Lock erzwingt.

Quelle / Beleg. git ls-files mcp-papierkram/ zeigt nur 4 Files, kein lock. pyproject.toml Zeilen 8–12.


F2 — Raw-Escape-Hatches default aktiviert

Severity:    MED
Confidence:  8/10
Status:      VERIFIED
Phase:       6 — Webhook + MCP-Endpoint-Auth (Tool-Surface)
Category:    Tool-Surface / Least-Privilege
File:        ~/source/mcps/mcp-papierkram/src/mcp_papierkram/server.py:66-71, 1510-1547

Was ist das Problem. PAPIERKRAM_EXPOSE_RAW defaultet auf true, d.h. die vier Raw-Tools (raw_get, raw_post, raw_put, raw_delete) werden registriert. Sie machen jeden API-Pfad inklusive Writes erreichbar, ohne dass das Tool-Schema die Pfade einschraenkt. Die dedizierten Tools sind nur weil-bequemer-Komfort, nicht weil-sicherer.

mcp-vf-hosted/infra/lib/mcp-vf-hosted-stack.ts:141 setzt PAPIERKRAM_EXPOSE_RAW: 'false' — der hosted Pfad ist also gehaertet. Aber: wer das MCP eigenstaendig deployed (z.B. ueber Cloudflare-Tunnel ohne den mcp-vf-hosted-Wrapper) bekommt Raw-Tools live ohne Schutz-Layer. Default-Sicher waere false.

Exploit-Skizze. Operator deployed mcp-papierkram standalone hinter einem Cloudflare-Tunnel mit ScalekitProvider, vergisst die ENV-Variable. Ein LLM-User (claude.ai) bekommt Raw-Tools angeboten. Bei Prompt-Injection (z.B. Kommentar im Papierkram-Rechnungstext eines manipulierten Kundendatensatzes) kann das Modell raw_delete("/income/invoices/<id>") aufrufen — die dedizierten Tools haben das Loesch-Risiko, die Raw-Variante eskaliert die Pfad-Freiheit ohne Mehrwert im hosted Use-Case.

Fix. Default-Flag flippen: RAW_ENABLED = os.environ.get("PAPIERKRAM_EXPOSE_RAW", "false").... Local-Debug-Sessions muessen die Flag explizit setzen. README anpassen + Warn-Log beim Start wenn aktiviert.

Quelle / Beleg. server.py:66-71 (Default-Wert), 1538-1547 (Registrierungs-Gate). cdk.out + lib/mcp-vf-hosted-stack.ts:141 (Defense-in-Depth-Override im hosted Pfad).


F3 — HTTP-Bind ohne Auth-Sanity-Check

Severity:    MED
Confidence:  8/10
Status:      VERIFIED
Phase:       6 — Webhook + MCP-Endpoint-Auth
Category:    Misconfiguration / Network-Surface
File:        ~/source/mcps/mcp-papierkram/src/mcp_papierkram/server.py:1555-1565

Was ist das Problem. main() bindet HTTP auf MCP_HTTP_HOST (default 127.0.0.1). Ein Operator kann das auf 0.0.0.0 setzen, ohne dass FastMCP eine eigene Auth-Schicht erzwingt — und der MCP hat keine eigene Auth, nur den Papierkram-Bearer-Token zum Upstream. Wer den Port public exposed (z.B. ECS-Service ohne Sidecar, Hetzner-Server ohne UFW), gibt 96 Tools inkl. ~62 Write-Tools auf den eigenen Papierkram-Account frei.

Exploit-Skizze. Internal-Misconfig-Szenario: Operator deployed das MCP auf eine Hetzner-VM mit Cloudflare-Tunnel-DNS, vergisst die Hostname-Variable und setzt MCP_HTTP_HOST=0.0.0.0 damit der Tunnel-Container reinkommt. Ohne UFW oder Cloudflare-Access-Policy ist der Port auch direkt von der Public-IP erreichbar. Jeder, der die IP findet, kann delete_invoice, cancel_voucher, create_voucher etc. ohne Auth ausloesen.

Fix. Bei host != "127.0.0.1" und nicht-localhost-Bind einen Warn-Log emittieren („MCP bindet auf NON-LOCAL host ohne eigene Auth — vor einen Reverse-Proxy mit Auth setzen”). Optional: Hard-Refusal wenn weder 0.0.0.0-allow-Flag noch Auth-Provider konfiguriert sind. README-Section „Production-Hosting” ergaenzen.

Quelle / Beleg. server.py:1555-1565 (main()-Logik), keine Auth-Sanity-Check zu sehen.


Triage / Severity-Verteilung

SeverityCountFindings
CRIT0
HIGH0
MED3F1 (Supply-Chain), F2 (Raw-Tools-Default), F3 (Bind-Address)
LOW0

Daily-8/10-Gate: 3 Findings ueberleben. tief-Modus wuerde voraussichtlich noch 2-3 TENTATIVE-Findings draufpacken (data: dict ohne Pydantic-Model, PDF-Size-Cap, 404-Hint-Echo).

Remediation-Plan Critical + High

Keine. Keine Remediation-Pflicht aus Daily-Sicht. Die drei MEDs sind Hygiene und gehoeren in Phase 2 (Pattern-File) als „pre-public-baseline” — vor einem oeffentlichen Audit-Report wuerde man die fixen wollen.

Aufwand-Schaetzung pro Finding

IDFix-Effort
F1 (uv.lock + frozen)30 min — uv lock, commit, Dockerfile uv sync --frozen flag (in mcp-vf-hosted).
F2 (Raw default flip)15 min — 1 Zeile flippen, README ergaenzen, Warn-Log.
F3 (Bind-Sanity)30 min — main() mit Host-Check + Warn-Log, README-Hosting-Section.

Summe: ~1.5h alle drei fixen, ein Commit, kein Schema-Break.

Aktive Verifikation

Pro Finding Code-Trace bestaetigt (nicht live getestet — Audit-Skill-Regel: NIE gegen live API).

Baseline

Siehe baseline.json im selben Run-Ordner (intern/runs/2026-05-18-security-audit-mcp-papierkram/).