Zum Inhalt springen
v1.1.0EN

Projekt

Sicherheitsrichtlinie

Auf dieser Seite

Eine Schwachstelle melden#

Bitte eröffnen Sie kein öffentliches Issue. Melden Sie Schwachstellen vertraulich über GitHub Private Vulnerability Reporting. Sie erhalten innerhalb von 72 Stunden eine Eingangsbestätigung und für bestätigte Probleme innerhalb von 14 Tagen eine Behebung oder einen Plan zur Abhilfe. Im Advisory werden Sie genannt, sofern Sie das nicht anders wünschen.

Sicherheitskorrekturen erhält nur die neueste veröffentlichte Version.

Bedrohungsmodell#

Dieser Server steht zwischen einem KI-Modell und einem sevDesk-Konto. Der Entwurf geht davon aus:

  1. Das sevDesk-API-Token kann alles. sevDesk-Tokens haben keine einschränkbaren Rechte und laufen nie ab; sie können alles, was ihr Benutzer kann, im gesamten Konto. Der Server setzt deshalb seine eigenen minimalen Rechte durch: Werkzeuge sind in Stufen gruppiert (read, write, commit, external), nur read ist standardmäßig aktiv, und Werkzeuge oberhalb der erlaubten Stufen werden nie registriert. Die Stufe wird erneut im Dispatcher und im HTTP-Client durchgesetzt, der außerdem die Mindeststufe jedes Endpunkts kennt (zum Beispiel brauchen sendBy, bookAmount, enshrine, cancelInvoice, das Fertigstellen eines Belegs und das Ändern eines Kontoumsatzes die Stufe commit; sendViaEmail braucht external) und eine Anfrage eines Werkzeugs mit zu niedriger Stufe ablehnt.
  2. Das Modell kann manipuliert werden. Text, der in sevDesk gespeichert ist (Kontaktnamen, Beschreibungen von Rechnungen und Belegen, Verwendungszwecke von Überweisungen, Inhalte von E-Rechnungen), kann von Dritten stammen und Prompt Injection enthalten. Solcher Text wird als nicht vertrauenswürdige Daten behandelt: In der Ausgabe ist er eingefasst, unsichtbares Unicode wird entfernt, und er erscheint nie in Fehlermeldungen. Rechtlich bindende oder nach außen wirkende Aktionen (commit, external) erfordern eine Bestätigung durch einen Menschen außerhalb der Kontrolle des Modells (MCP-Elicitation oder eine Freigabeseite, siehe unten); ein confirm-Flag des Modells wird nie als Freigabe angenommen.
  3. Es gibt keine Testumgebung. Jeder Schreibvorgang ist echt. Schreibvorgänge werden nie automatisch wiederholt (um Dubletten zu vermeiden), zur Prüfung zurückgelesen, können als Probelauf laufen (dryRun: true oder SEVDESK_DRY_RUN=1: Das Werkzeug liest, prüft und gibt seine Vorschau zurück, sendet nichts und fragt niemanden) und können in einem lokalen Audit-Log festgehalten werden.
  4. Geheimnisse dürfen nicht nach außen gelangen. Das Token wird aus der Umgebung, einer Datei oder dem Schlüsselbund des Betriebssystems gelesen; es wird nie protokolliert, ausgegeben oder in Fehler aufgenommen. Logs gehen nur nach stderr. Im Fernzugriff gilt dasselbe für die Bearer- und Server-Geheimnisse, verschlüsselte Tokens, Autorisierungscodes und Cookies.
  5. Auch lokale Dateien sind eine Grenze. Das Modell nennt Dateien; der Server darf also nicht zu einem Weg werden, beliebige Dateien zu lesen oder zu schreiben. Werkzeuge lesen nur innerhalb von SEVDESK_FILE_DIRS (absolute Ordner; echte Pfade werden geprüft, symbolische Links dürfen nicht nach außen führen, die Datei wird geöffnet, ohne einem Link zu folgen, und über ihre Inode geprüft, ihr Typ wird am Inhalt erkannt, Größen sind begrenzt) und schreiben nur innerhalb von SEVDESK_OUTPUT_DIR (nur neue Dateien, Modus 0600, nie überschreibend). Ohne diese Einstellungen liest oder schreibt kein Werkzeug lokale Dateien. Empfangene E-Rechnungen sind feindliche Eingaben: Der XML-Parser lehnt DOCTYPEs und Entitäten ab und begrenzt Größe und Verschachtelungstiefe, der Leser für PDF-Anhänge begrenzt die entpackten Größen, und jedes ausgelesene Textfeld gilt als nicht vertrauenswürdig. Das ZIP des DATEV-Exports wird über einen vorsignierten Link geholt, den sevDesk zurückgibt; der Server akzeptiert dafür genau einen https-Host, folgt keinen Weiterleitungen, sendet keine Zugangsdaten, begrenzt die Größe und zeigt den Link nie dem Modell.
  6. Der Fernzugriff fügt eine Netzwerkgrenze hinzu. Der HTTP-Transport startet nicht ohne Authentifizierung, lauscht standardmäßig auf 127.0.0.1 (jede andere Adresse braucht eine konfigurierte öffentliche URL) und prüft Host und Origin. Im OAuth-Modus ist er standardmäßig auf eigene Konten beschränkt (nur aufgeführte sevDesk-Konten können sich verbinden, nur erlaubte Clients können fragen), speichert keine sevDesk-Tokens (das Token jedes Benutzers reist verschlüsselt in den Tokens, die sein Client hält) und ordnet Freigaben, Bestätigungen und Caches dem Konto und Zugang jeder Anfrage zu. Siehe Fernzugriff.

Nicht im Umfang: Schwachstellen in sevDesk selbst (melden Sie diese der sevdesk GmbH) und Angriffe, die die Kontrolle über den Rechner voraussetzen, auf dem der Server läuft.

Bestätigung durch einen Menschen#

Jeder Aufruf der Stufen commit und external zeigt dem Menschen eine Vorschau, die das Werkzeug aus dem gerade Gelesenen erstellt: die Aktion in einem Satz, dann die Warnungen und „This cannot be undone.“ bei unumkehrbaren Aktionen, dann die wichtigsten Angaben (Datensatz, Kunde, Beträge, Daten, Empfänger, Texte). Aus den Werten werden unsichtbare, Bidi- und Steuerzeichen entfernt, damit gespeicherter Text dem Menschen nichts verbergen kann. Werte werden vollständig mit ihren Zeilenumbrüchen gezeigt (etwa ein E-Mail-Text), jede Zeile unter ihrer Beschriftung eingerückt, damit keine Zeile eines Werts sich als Beschriftung oder Warnung ausgeben kann; ein Wert über 12.000 Zeichen endet mit „[… N more characters not shown; open the dry run to see everything]“, statt stillschweigend gekürzt zu werden, und der Probelauf gibt ihn vollständig zurück. Die Freigabe bindet einen Hash der vollständigen Vorschau, nicht dessen, was auf den Bildschirm passte. Nur die Antwort des Menschen zählt: Kein Argument eines Werkzeugs kann etwas freigeben.

Umgekehrt darf auch Text, den der Mensch nicht sehen kann, sevDesk nicht erreichen. Bevor etwas gelesen oder gesendet wird, lehnt jedes Schreibwerkzeug (Probeläufe eingeschlossen) Argumente ab, die Zeichen enthalten, die ein Mensch so nicht sehen würde: Steuer- und Formatzeichen (Nullbreite, Bidi), andere standardmäßig ignorierbare Codepunkte (Variantenselektoren, U+034F, Hangul-Füller), Wagenrückläufe, die Zeilen- und Absatztrenner sowie Tabulatoren. Der Fehler nennt das Argument und die Codepunkte; Zeilenumbrüche sind erlaubt.

SEVDESK_CONFIRM=elicit (Standard). Der Aufruf antwortet mit einer MCP-Elicitation über den Ablauf mit mehreren Runden aus 2026-07-28 (input_required): ein Formular mit einem einzigen Schalter „Approve this action“, der zunächst aus ist. Der ausstehende Zustand reist als requestState durch den Client, signiert mit HMAC-SHA256 (der Request-State-Codec des SDK, ein zufälliger 256-Bit-Schlüssel je Serverprozess, 10 Minuten gültig, gebunden an die Methode, den Client und im HTTP-Modus an den Zugang und das Konto). Er bindet den Namen des Werkzeugs, einen SHA-256 der Argumente (ohne dryRun), einen SHA-256 der Vorschau und eine Einmal-Nonce. Beim erneuten Versuch lehnt der Server einen Zustand für andere Argumente oder ein anderes Werkzeug ab, lehnt eine wiederverwendete Freigabe ab (die Nonce wird einmal verbraucht), bereitet die Aktion erneut vor und fragt, falls sich die Vorschau in der Zwischenzeit geändert hat (der Datensatz wurde bearbeitet), erneut, statt auszuführen. Ablehnen, Abbrechen oder ein nicht umgelegter Schalter ergeben declined: Nichts ändert sich. Für Clients aus der Zeit von 2025 sendet die Kompatibilitätsschicht des SDK eine echte elicitation/create-Anfrage über dieselbe Verbindung, sodass dieselben Prüfungen gelten. Clients, die keine Formular-Elicitation angeben, und Clients aus der Zeit von 2025 über zustandsloses HTTP (die keine Anfragen vom Server an den Client empfangen können) bekommen einen Fehler, der dem Benutzer sagt, zu browser zu wechseln; nichts wird gesendet.

SEVDESK_CONFIRM=browser über stdio. Für Clients ohne Elicitation legt der Server eine Freigabeseite an, die er selbst nur auf 127.0.0.1 und einem flüchtigen Port bereitstellt, und öffnet sie im Browser des Benutzers mit dem Öffner des Betriebssystems (open, xdg-open oder cmd /c start, gestartet mit einem Argument-Array, ohne Shell). SEVDESK_CONFIRM_OPEN=0 schaltet das Öffnen ab. Der Link der Seite ist geheim: Er geht in das Log des Servers (stderr, eine Zeile je Freigabe) und an den Öffner, nie an das Modell, das Ergebnis des Werkzeugs oder das Audit-Log. Der Aufruf gibt approval_pending mit einem kurzen, nicht geheimen Referenzcode zurück (auch auf der Seite gezeigt, damit der Benutzer beide zuordnen kann), den Ablauf und ob die Seite geöffnet wurde. Jede Freigabe bindet Werkzeug, Hash der Argumente und Hash der Vorschau, hat eine zufällige 256-Bit-ID, läuft nach 10 Minuten ab und wird einmal verwendet; ruft das Modell das Werkzeug nach der Freigabe durch den Menschen mit denselben Argumenten erneut auf, bereitet der Server erneut vor und führt die Aktion nur aus, wenn die Vorschau unverändert ist. Die Seite ist statisches HTML, jeder Wert ist maskiert, unter einer strengen Content Security Policy (keine Skripte, kein Einbetten in Frames, Formulare nur an sich selbst). Approve und Reject sind POST-Formulare, die ein CSRF-Token der Seite je Freigabe tragen müssen, ein SameSite=Strict-Cookie je Freigabe, das beim Laden der Seite gesetzt wurde, Host: 127.0.0.1:<port> (jede Anfrage wird auf den Host geprüft, gegen DNS-Rebinding), und von der Seite selbst kommen müssen: Origin: http://127.0.0.1:<port> oder, bei Clients, die kein Origin senden, das vom Browser gesetzte Sec-Fetch-Site: same-origin. Origin: null wird abgelehnt; die Seite verwendet die Referrer-Policy same-origin, unter der Browser den echten Origin senden (unter no-referrer würden sie null senden). Höchstens fünf Freigaben warten gleichzeitig auf eine Entscheidung; darüber hinaus werden Aufrufe abgelehnt, bis der Benutzer entscheidet oder die Anfragen ablaufen, damit ein Assistent in einer Schleife den Browser nicht mit Tabs fluten kann. Im Fernzugriff nutzt browser stattdessen das Freigabe-Postfach des Servers (siehe Fernzugriff).

Grenze des Browser-Modus. Das Modell sieht den Link nie, aber der Link ist trotzdem eine Berechtigung. Ein Agent, der das stderr-Log des Servers lesen kann (zum Beispiel ein Client, der dem Modell MCP-Logs zeigt, oder Shell-Zugriff auf die Logdatei) oder der den Browser des Benutzers steuern kann (eine offene Seite, Browser-Automatisierung), kann trotzdem selbst freigeben. Der Browser-Modus schützt daher vor einem Modell, das nur redet (zum Beispiel einem, das durch Prompt Injection in sevDesk-Daten gesteuert wird), nicht vor einem Agenten mit solchem Zugriff. elicit bleibt der empfohlene Modus, besonders in agentischen Clients, die Befehle ausführen können.

SEVDESK_CONFIRM=off wird nur zusammen mit SEVDESK_I_UNDERSTAND_NO_CONFIRM=1 angenommen.

Audit-Log#

Mit SEVDESK_AUDIT_LOG=<path> hängt jeder schreibende Aufruf JSON Lines an diese Datei an: dry_run, confirmation_requested (mit der Vorschau und im Browser-Modus dem Referenzcode), confirmation_refused (eine wiederverwendete, abgelaufene oder nicht passende Bestätigung), approval_decided (Approve oder Reject auf der Freigabeseite gedrückt, mit dem Referenzcode), declined, executing (mit der Art der Freigabe: elicit, browser, confirmation_off oder not_required für die Stufe write), executed (mit den geänderten Datensätzen) und failed (mit der Art des Fehlers und allen Datensätzen, die ein früherer Schritt bereits geändert hat). Jede Zeile hat einen Zeitstempel, eine Aufruf-ID, die Anfrage, Entscheidung und Ausführung teilen, das Werkzeug, seine Stufe, den Hash der Argumente, den Hash der Vorschau, die gezeigt und freigegeben wurde, und die Argumente (Token geschwärzt, versteckte Zeichen als sichtbare \u{…}-Escapes geschrieben, Zeichenketten bis 20.000 Zeichen, längere markiert). Im Fernzugriff hat jede Zeile außerdem account (die sevDesk-Konto-ID; bearer im Bearer-Modus, falls die Abfrage des Kontos beim Start fehlschlug) und grant (die ID des Zugangs oder bearer), und ein Log umfasst jedes Konto, das der Server bedient. Die Datei wird mit Modus 0600 angelegt (und auf 0600 verschärft, falls sie schon existiert), nur zum Anhängen geöffnet und nie über einen symbolischen Link. Kann die Zeile executing nicht geschrieben werden, wird die Aktion nicht ausgeführt und nichts gesendet (fail closed). Das Log ist lokal und nicht fälschungssicher: Es hält für den Benutzer fest, was dieser Server getan hat, und schützt nicht vor jemandem, der den Rechner kontrolliert. Jede Zeile beginnt mit ts und der Formatversion v (derzeit 1).

Generischer Lesezugriff#

sevdesk_api_get (Werkzeuggruppe advanced, standardmäßig aus) lässt das Modell einen GET-Pfad wählen, was ein kuratiertes Werkzeug nie tut. Es hat vier unabhängige Schutzmechanismen:

  1. Das konfigurierte sevDesk-Token irgendwo im Pfad oder in der Query wird abgelehnt, und die Ablehnung wiederholt den Pfad nicht.
  2. genericGetRefusal lehnt, bevor etwas gesendet wird, Endpunkte ab, die Zustand ändern, Dateien liefern oder Hintergrundaufgaben starten (getPdf, getXml, render, sendBy, bookAmount, enshrine, cancelInvoice, die Endpunkte zum Zurücksetzen, getNext…Number, Segmente, die mit einem verändernden Verb beginnen, und alles unter Export, ExportJob, Progress, Report, DocServer und SevSequence), sowie gefährliche Query-Schlüssel: Zugangsdaten (sevDesk nimmt ?token= an), Schlüssel zum Blättern, die das Werkzeug selbst setzt, download, preventSendBy, enshrine…, useNextNumber und Methoden-Overrides. Query-Schlüssel werden so normalisiert, wie PHP sie liest; so werden auch Api.Key oder token[0] erkannt.
  3. Der Client ohne Schreibrechte lehnt jedes GET mit einer Nebenwirkung ab, das resolveGetSafety erkennt.
  4. Der HTTP-Client lehnt es erneut ab, weil ein Lesezugriff keine Stufe angibt.

Antworten werden mit einer Grenze von 1 MiB gelesen, müssen JSON in der {objects: …}-Hülle von sevDesk sein und werden wie nicht vertrauenswürdiger Text bereinigt: unsichtbare Zeichen entfernt, Zeichenketten begrenzt, Binärinhalte (Dateien in Base64, Vorschaubilder) und Echos von sevClient entfernt und aufgeführt, höchstens etwa 100 KB zurückgegeben. Das verbleibende Risiko ist ein undokumentiertes GET mit einer Nebenwirkung, das keine dieser Regeln erfasst; deshalb ist die Werkzeuggruppe standardmäßig aus.

Lokale Token-Ablage und Profile#

Über stdio kommt das Token aus der Umgebung, einer Datei (SEVDESK_API_TOKEN_FILE) oder dem Schlüsselbund des Betriebssystems (SEVDESK_API_TOKEN_KEYCHAIN).

  • Schlüsselbund. Einträge liegen unter dem festen Dienst mcp-server-sevdesk; die Konfiguration nennt nur die Bezeichnung, sodass eine feindliche Konfiguration den Server nicht dazu bringen kann, einen anderen Schlüsselbund-Eintrag (ein Passwort, ein anderes Token) zu lesen und an eine eigene SEVDESK_BASE_URL zu senden. Der Server führt das Hilfsprogramm des Betriebssystems (/usr/bin/security unter macOS, secret-tool unter Linux) mit einem Argument-Array aus, nie über eine Shell. Beim Ablegen reist das Token über stdin des Hilfsprogramms, nie in seinen Argumenten, die andere lokale Benutzer in der Prozessliste sehen könnten. Die Ausgabe des Hilfsprogramms ist begrenzt, wird nie protokolliert oder ausgegeben, und Fehler sind feste Texte. Der Schlüsselbund schützt das Token im Ruhezustand, in Konfigurationsdateien, Backups und Screenshots; er schützt nicht vor Code, der bereits als derselbe Benutzer läuft. Unter macOS vertraut der Eintrag /usr/bin/security, das ihn angelegt hat; jeder Prozess des Benutzers kann ihn also auf diesem Weg ohne Dialog lesen. Windows wird noch nicht unterstützt und mit einem Hinweis auf SEVDESK_API_TOKEN_FILE abgelehnt.
  • Die Kommandozeile. token set liest das Token von stdin (in einem Terminal verdeckt), nie aus einem Argument, wo es in der Shell-Historie bliebe. Es prüft das Token bei sevDesk und legt nichts ab, was sevDesk ablehnt. Bezeichnungen, Profilnamen und unbekannte Argumente, die wie ein Token aussehen, werden nie ausgegeben.
  • Profile. Ein Profil kann Stufen und Ordner erweitern; unter macOS und Linux müssen die Profildatei und ihr Ordner (auch der echte Ordner hinter einem symbolischen Link) daher dem Benutzer oder root gehören und dürfen für Gruppe und andere nicht beschreibbar sein; sonst wird die Datei abgelehnt. Unter Windows wird das nicht geprüft. Profile enthalten nie ein Token, nur den Ort, an dem es aufbewahrt wird, und das token.env eines Profils darf nur SEVDESK_TOKEN_*-Variablen nennen, sodass ein Profil kein beliebiges Geheimnis aus der Umgebung an eine eigene Basis-URL senden kann. Eine Variable, die sowohl in der Umgebung als auch im Profil gesetzt ist, ist ein Fehler; nur SEVDESK_READ_ONLY=1 und SEVDESK_DRY_RUN=1 in der Umgebung dürfen ein Profil stillschweigend verschärfen. Meldungen über die Datei zitieren nie ihren Inhalt.
  • Der HTTP-Modus lehnt SEVDESK_PROFILE, SEVDESK_PROFILES_PATH und SEVDESK_API_TOKEN_KEYCHAIN ab: Ein Server hat keinen Benutzer-Schlüsselbund, und seine Konfiguration gehört in seine Umgebung.

Claude-Desktop-Erweiterung#

Das .mcpb-Paket betreibt denselben Server über stdio auf dem eingebauten Node.js von Claude Desktop. Claude Desktop speichert das Token, eine als sensibel markierte Einstellung, im sicheren Speicher des Betriebssystems (Schlüsselbund unter macOS, Anmeldeinformationsverwaltung unter Windows) und übergibt es dem Server beim Start als SEVDESK_API_TOKEN; das Token erscheint nie in der Kommandozeile. Das Paket enthält den Server und alle seine Abhängigkeiten in einer Datei, die nur eingebaute Module von Node.js importiert, mit den Lizenzhinweisen jedes gebündelten Pakets. Die Bestätigung lässt sich in der Erweiterung nicht abschalten: Sie bietet keine Möglichkeit, SEVDESK_I_UNDERSTAND_NO_CONFIRM zu setzen. Das Paket ist noch nicht signiert; prüfen Sie einen Download mit seiner Provenance-Attestierung (siehe Lieferkette).

Fernzugriff#

Der HTTP-Transport (--http, Anleitung für den Betrieb in docs/remote.de.md) fügt eine Netzwerkgrenze hinzu und im OAuth-Modus mehrere Konten und Verbindungen in einem Prozess. Die Entscheidungen sind ADR 0006 und ADR 0007.

Netzwerk. Der Server startet nicht ohne Authentifizierung. Er lauscht standardmäßig auf 127.0.0.1 und braucht für jede andere Adresse eine öffentliche URL, und eine öffentliche URL muss https verwenden, außer ihr Host ist ein Loopback-Name. Der Host jeder Anfrage muss einer der konfigurierten Namen sein (gegen DNS-Rebinding; nur /healthz ist ausgenommen, für die Zustandsprüfungen von Plattformen). Ein Origin-Header an /mcp muss einen erlaubten Host nennen (die Prüfung vergleicht Hostnamen; /mcp sendet keine CORS-Header, sodass Webseiten auf anderen Origins es nicht aufrufen können), und Formular-POSTs müssen von den eigenen Seiten des Servers kommen (Origin: null wird abgelehnt). Bodys sind je Route begrenzt und müssen in kurzer Zeit eintreffen, Anfragen haben Fristen, Ratenbegrenzungen gelten je Client-Adresse (IPv6 je /64-Netz), und die Zahl gleichzeitig laufender Anfragen ist so begrenzt, dass ein Client sie nicht für alle aufbrauchen kann. Antworten tragen nosniff, no-store, no-transform (damit Proxys Seiten nicht umschreiben), eine Referrer-Policy und über https HSTS. URLs, die der Server ausgibt oder auf die er weiterleitet, werden aus der konfigurierten öffentlichen URL gebaut, nie aus Headern der Anfrage. SEVDESK_HTTP_TRUST_PROXY muss der tatsächlichen Zahl der Proxys davor entsprechen: Ein höherer Wert lässt Clients die IP-Adresse wählen, die die Ratenbegrenzung sieht. Einfaches http wird nur für eine öffentliche Loopback-URL angenommen; beschränken Sie eine solche Einrichtung auf kurze lokale Tests (siehe die Grenzen des Postfachs unten).

Bearer-Modus. Ein gemeinsames Geheimnis (zwei während eines Wechsels), mindestens 32 Zeichen mit mindestens 16 verschiedenen und nie gleich dem sevDesk-Token. Der Server vergleicht ein HMAC des vorgelegten Werts mit einem HMAC jedes konfigurierten Werts per timingSafeEqual, ohne beim ersten Treffer aufzuhören. Wer das Geheimnis hält, kann die Stufen des Servers für dessen Konto nutzen. Der Start warnt, wenn ein Bearer- oder Server-Geheimnis eher wie eine Passphrase als wie zufällige Bytes aussieht.

OAuth-Zustimmung und Erlaubnislisten. Die Zustimmungsseite zeigt den Host der client_id des Clients, den selbst angegebenen Namen des Clients (bereinigt und als solcher gekennzeichnet) und den Host, auf den sie weiterleiten wird, mit einer Warnung, wenn jede Weiterleitung auf eine Loopback-Adresse geht (eine App auf dem Rechner des Benutzers). Nur read ist vorausgewählt; andere Stufen müssen angehakt werden. So weit kommen Clients nur, wenn ihr Metadaten-Dokument auf einem Host in SEVDESK_OAUTH_CLIENTS liegt (Standard claude.ai), geprüft, bevor etwas abgerufen wird: Ein Angreifer kann Ihre Zustimmungsseite nicht seinen eigenen Client präsentieren lassen (Consent-Phishing). Nur sevDesk-Konten in SEVDESK_OAUTH_ACCOUNTS können die Zustimmung abschließen; ein Server, dessen URL bekannt wird, wird so nicht zu einem Dienst für die Konten Fremder; ein abgelehntes Konto wird als account_refused protokolliert. Beide Erlaubnislisten gelten auch für jede Anfrage und Erneuerung: Wird ein Konto oder Client-Host entfernt und der Server neu gestartet, werden seine bestehenden Zugänge bei der nächsten Nutzung widerrufen, und die Postfach-Sitzungen des Kontos enden. Eine geprüfte Zustimmung lässt sich nur einmal abschließen, und jedes Konto hält eine begrenzte Zahl gültiger Zugänge (eine neue Zustimmung darüber hinaus ersetzt den ältesten des Kontos), damit ein Konto den Zugangsspeicher nicht für alle anderen füllen kann. Die Seite verwendet ein SameSite=Strict-CSRF-Cookie mit einem passenden HMAC-Feld, nimmt POSTs nur von sich selbst an, lässt sich nicht in Frames einbetten, und ihre Content Security Policy sendet Formulare nur an sich selbst und an den geprüften Origin der Weiterleitung. Fehler im Client, seinen Metadaten oder der Redirect-URI zeigen eine Fehlerseite und leiten nie weiter. Das eingefügte sevDesk-Token wird bei sevDesk geprüft und nie wieder angezeigt.

Metadaten von Clients abrufen (SSRF). Eine client_id-URL abzurufen ist eine Anfrage, die der Server für jemand anderen stellt. Die URL muss https auf einem DNS-Namen sein, ohne Query, Fragment, Zugangsdaten oder Punkt-Segmente, auf einem erlaubten Host. Der Name wird einmal aufgelöst, jede Adresse muss öffentlich sein (private, Loopback-, Link-Local-, CGNAT-, Unique-Local-, Dokumentations- und Multicast-Bereiche werden abgelehnt, auch innerhalb von NAT64-, 6to4- und IPv4-gemappten Adressen), und die Verbindung geht an die geprüfte Adresse. Keine Weiterleitungen, 3 Sekunden für den Verbindungsaufbau und 5 insgesamt, nur application/json, höchstens 5 KiB. Das Dokument muss dieselbe client_id nennen, 1–10 Redirect-URIs aufführen und darf kein Client-Secret haben. Ablehnungen werden als cimd_blocked protokolliert.

OAuth-Tokens und Widerruf. Nur PKCE mit S256; der Hash des Verifiers wird in konstanter Zeit verglichen. Redirect-URIs müssen genau übereinstimmen (Loopback-Weiterleitungen: der Host genau, beliebiger Port). Autorisierungscodes leben 60 Sekunden und werden einmal verwendet: Ein zweiter Austausch widerruft den Zugang (code_replay). Access-Tokens leben 1 Stunde und gelten nur für <public URL>/mcp (RFC 8707, genaue Übereinstimmung), und jede Anfrage schlägt den Zugang nach, sodass ein Widerruf mit der nächsten Anfrage wirkt. Refresh-Tokens werden bei jeder Nutzung ersetzt; das vorige wird nach einer Rotation noch 30 Sekunden angenommen, jede spätere Nutzung widerruft den Zugang (refresh_replay). Jede Erneuerung prüft das sevDesk-Token erneut bei sevDesk, und ein 401 widerruft den Zugang. Ein Zugang endet spätestens nach 90 Tagen. Alle Tokens sind mit AES-256-GCM unter einem Schlüssel je Zweck verschlüsselt, mit authentifiziertem Typ, Issuer und Schlüssel-ID, und tragen ein lesbares Präfix sdmcp_, damit Schwärzung und Secret-Scanner sie finden. Der Zugriff endet mit grants revoke (ein Zugang, ein Konto oder alle), dem /revoke-Aufruf des Clients (RFC 7009), einem neuen Server-Geheimnis ohne _PREVIOUS (alles) oder einem neu erzeugten sevDesk-Token. Ein neu erzeugtes Token stoppt Werkzeugaufrufe sofort, weil sevDesk das alte ablehnt, und widerruft jeden Zugang, der es hält, bei dessen nächster Erneuerung; es beendet keine Postfach-Sitzungen, denn eine Anmeldung mit eingefügtem Token ist nicht an dieses Token gebunden (sie gilt bis zu 12 Stunden), und eine Sitzung aus einer Zustimmung gilt, bis ihr Zugang widerrufen wird. grants revoke --account <id> beendet die Zugänge und jede Postfach-Sitzung des Kontos auf einmal. Ein Server, der seine Sperre auf das Zustandsverzeichnis an einen anderen verliert, vertraut seinem Zugangsspeicher nicht mehr (OAuth-Anfragen scheitern mit einem Serverfehler) und beendet sich, sodass er nie aus Zugängen antwortet, die er nicht mehr aktuell halten kann.

Was das Server-Geheimnis entschlüsseln kann. Jeder Schlüssel stammt aus SEVDESK_HTTP_SECRET. Wer es hat und einen Autorisierungscode, ein Access- oder Refresh-Token oder den Formularzustand einer Zustimmungsseite abfängt (von einem Client, aus einem Proxy-Log, von einer Middlebox, die TLS terminiert), kann das sevDesk-Token darin lesen. Das Geheimnis allein liefert kein sevDesk-Token, weil der Server keines speichert. Bewahren Sie es in einem Secret-Speicher auf (SEVDESK_HTTP_SECRET_FILE), nie im Image, im Repository oder in einer Deploy-Vorlage, und schützen Sie auch das Zustandsverzeichnis: Mit dem Geheimnis und einer Kopie davon könnte jemand eine Postfach-Sitzung für eine aufgeführte Sitzungs-ID fälschen. Wenn das Geheimnis nach außen gelangt sein könnte, ersetzen Sie es ohne _PREVIOUS (jeder Client verbindet sich neu); wenn auch Tokens nach außen gelangt sein könnten, erzeugen Sie die sevDesk-Tokens neu und führen grants revoke --account <id> für die betroffenen Konten aus.

Das Freigabe-Postfach. Im Fernzugriff stellt SEVDESK_CONFIRM=browser ein Postfach unter <public URL>/approvals bereit. Seine URL ist nicht geheim: Das Modell sieht sie im Ergebnis des Werkzeugs, und ohne Sitzung zeigt das Postfach nur ein Anmeldeformular, keine Daten von Freigaben. Ein Browser erhält eine Sitzung bei der OAuth-Zustimmung oder durch Einfügen des sevDesk-Tokens (OAuth: bei sevDesk und gegen die Kontenliste geprüft; Bearer: in konstanter Zeit mit dem SEVDESK_API_TOKEN des Servers verglichen, nie mit dem Bearer-Geheimnis, das der Client hält). Anmeldungen sind auf 5 pro Minute und 20 pro Stunde je Client-Adresse begrenzt (IPv6 je /64-Netz). Das Sitzungs-Cookie ist verschlüsselt und enthält kein sevDesk-Token, ist HttpOnly, Secure, SameSite=Lax und mit __Host- präfixiert, gilt 12 Stunden, bekommt bei jeder Anmeldung eine neue ID und endet mit seinem Zugang oder mit grants revoke --account. Lax ist nötig, weil dem Link von claude.ai aus gefolgt wird, einer seitenübergreifenden Navigation; Entscheidungen sind POSTs, die außerdem ein an die Sitzung gebundenes CSRF-Token tragen und aus dem Postfach selbst kommen müssen (Origin oder Sec-Fetch-Site: same-origin; nie Origin: null). Jede Entscheidung prüft, dass die Freigabe zum Konto der Sitzung gehört. Eine Freigabe gehört dem Zugang, der gefragt hat: Ein erneuter Versuch aus einer anderen Verbindung, auch desselben Kontos, kann sie nicht nutzen. Freigaben binden weiterhin Werkzeug, Argumente und Vorschau, laufen nach 10 Minuten ab, werden einmal verwendet, und höchstens fünf je Konto warten gleichzeitig; die Gesamtgrenze des Speichers kann nicht von einem Konto genutzt werden, um die Freigaben eines anderen Kontos zu verdrängen.

Grenzen des Postfachs. Ein Agent, der einen angemeldeten Browser steuern kann (Browser-Automatisierung, ein Werkzeug zur Computersteuerung), kann trotzdem selbst freigeben: Wie die lokale Seite schützt das Postfach vor einem Modell, das nur redet. Eine nachgeahmte Seite könnte das sevDesk-Token abgreifen; fügen Sie es nur unter /approvals Ihres eigenen Servers ein (die meisten Benutzer müssen das nie, weil die Zustimmung den Browser anmeldet). Alle, die sich an einem Konto anmelden können, teilen sich sein Postfach und können über seine Freigaben entscheiden; sie halten ohnehin das Token dieses Kontos. Eine Sitzung bleibt in diesem Browser 12 Stunden gültig; melden Sie sich an einem gemeinsam genutzten Rechner also ab; ein neu erzeugtes sevDesk-Token beendet sie nicht (verwenden Sie grants revoke --account). Bei einer öffentlichen Loopback-URL über einfaches http (Entwicklung) hat das Cookie kein __Host--Präfix und kein Secure-Flag, und Browser trennen Cookies nicht nach Port: Jeder andere Webserver, den der Browser unter diesem Hostnamen erreicht, erhält das Sitzungs-Cookie und könnte Freigaben lesen und entscheiden. Verwenden Sie für alles über einen kurzen lokalen Test hinaus https oder ein eigenes Browserprofil. Ein Neustart macht ausstehende Freigaben ungültig.

Logs. Eine JSON-Zeile pro Anfrage auf stderr: die Routen-Vorlage, Status, Dauer, Client-IP und die ersten 8 Zeichen der Zugangs-ID. Header, Bodys, Cookies und Query-Strings werden nie protokolliert, und jede Zeile wird vom sevDesk-Token, den Bearer-Geheimnissen, dem Server-Geheimnis, verschlüsselten Tokens und jeder Folge von 32 oder mehr Hex-Ziffern bereinigt (die Form eines sevDesk-Tokens). Fehler, HTML-Seiten und Weiterleitungen enthalten nie ein Geheimnis, und das Zugangsobjekt im Speicher lässt das sevDesk-Token aus, wenn es serialisiert oder inspiziert wird. Sicherheitsereignisse werden mit Namen protokolliert (refresh_replay, code_replay, account_refused, cimd_blocked).

Was Aufgabe des Betreibers bleibt. TLS vor dem Server (ein Reverse Proxy, der Rand einer Plattform oder ein Tunnel; wer TLS terminiert, kann Tokens unterwegs sehen, wählen Sie also einen Anbieter, dem Sie vertrauen, und nutzen Sie schnelle Tunnel nur für Tests), das Server-Geheimnis, ein dauerhaftes Volume für das Zustandsverzeichnis, genau eine Instanz, enge Erlaubnislisten und das Aktualisieren des Images.

Fernzugriff: Dateien#

Mit aktivem SEVDESK_HTTP_FILES (der Standard) erreichen Dateien die Werkzeuge über Upload-Seiten und verlassen den Server als Download-Links (docs/remote.de.md, ADR 0008). Das Modell sieht und sendet nie den Inhalt einer Datei.

Links sind keine Berechtigungen. sevdesk_upload_request gibt <public URL>/uploads/<id> zurück und die liefernden Werkzeuge <public URL>/files/<id>: Das Modell sieht beide, also ist keiner geheim. Jede Seite braucht die Browser-Sitzung des Freigabe-Postfachs für das Konto des Datensatzes (gesetzt bei der OAuth-Zustimmung oder durch Anmeldung mit dem sevDesk-Token des Kontos; im Bearer-Modus nie mit dem Bearer-Geheimnis). Ohne Sitzung zeigt eine Seite nur das Anmeldeformular, dessen next nur die eigenen Datensatzpfade des Servers annimmt (keine offene Weiterleitung). Ein Datensatz eines anderen Kontos antwortet genau wie eine unbekannte ID (404). Werkzeuge lesen nur Uploads, die ihr eigener Zugang angefordert hat; ein anderer Zugang, auch desselben Kontos, erhält dieselbe Ablehnung wie für eine unbekannte ID. IDs sind 256 Bit zufällig; Fehlermeldungen, Zusammenfassungen und Logs enthalten sie nie.

Uploads. Die Seite zeigt nur vertrauenswürdige Werte (Zweck, angenommene Arten, Größengrenze, Konto, Referenz, Ablauf), sodass ein durch Prompt Injection gesteuertes Modell keine Anweisungen darauf setzen kann. Ein Upload ist ein POST vom selben Origin (Origin: null abgelehnt) mit einem an die Sitzung gebundenen CSRF-Token, genau den Feldern csrf und file, und besteht seine Prüfungen, bevor der Body gelesen wird: Ratenbegrenzung (10 pro Minute und 60 pro Stunde je Client), die Sitzung, ein offenes Ticket dieses Kontos, ein angegebenes Content-Length (411 ohne) innerhalb der Grenze (413) und höchstens zwei Bodys gleichzeitig (503). Der Dateityp wird am Inhalt geprüft (Belege PDF, PNG, JPEG; E-Rechnungen XML, PDF); kein ZIP wird angenommen, Bilder werden nie dekodiert, und der E-Rechnungs-Parser behält seine eigenen Schutzmechanismen gegen feindliches XML und PDFs. Namen werden auf [A-Za-z0-9._-] bereinigt (Umlaute ausgeschrieben, Pfadanteile, Steuer- und unsichtbare Zeichen, Anführungszeichen und Markup entfernt, höchstens 100 Zeichen, Endung aus dem Inhalt), immer maskiert und nie als Pfade verwendet. Jede Seite nimmt eine Datei an; Uploads werden nie wieder ausgeliefert.

Downloads. Die Bytes werden während des Werkzeugaufrufs von sevDesk geholt (PDFs mit preventSendBy=true); das Öffnen eines Links stellt keine Anfrage an sevDesk und hat keine Nebenwirkung. Antworten sind nur Anhänge (Content-Disposition: attachment mit einem ASCII- und einem RFC-5987-Namen), mit Content-Security-Policy: default-src 'none'; sandbox, nosniff, X-Frame-Options: DENY, Cross-Origin-Resource-Policy: same-origin, Referrer-Policy: no-referrer und Cache-Control: no-store: Eine Datei wird nie auf dem Origin des Servers dargestellt, wo XML Skripte ausführen könnte. HEAD wird abgelehnt. Ein Link liefert höchstens 5 Downloads.

Speicherung. Datensätze liegen nur im Speicher; die Bytes im Speicher oder in <stateDir>/files/ (Ordner 0700, Dateien 0600, exklusiv angelegt, ohne Links zu folgen, zufällige Namen), verschlüsselt mit AES-256-GCM unter einem Schlüssel, der beim Start des Prozesses zufällig erzeugt und nirgends gespeichert wird, mit authentifizierter Blob-ID, Konto und Art (ein zwischen Datensätzen vertauschter Blob lässt sich nicht öffnen). Dateien werden gelöscht, wenn sie ablaufen, und beim Herunterfahren, und der Ordner wird bei jedem Start geleert, nachdem die Sperre des Zustandsverzeichnisses übernommen wurde (ein zweiter Server rührt die Dateien eines laufenden nie an). Eine Kopie des Volumes oder des Server-Geheimnisses öffnet keine von ihnen. Upload-Seiten leben 15 Minuten, Uploads und Links SEVDESK_HTTP_FILES_TTL (30 Minuten, höchstens 24 Stunden).

Erschöpfung. Ein Upload ist höchstens 20 MiB groß; ein Konto hält höchstens 5 offene Upload-Seiten, 10 Uploads und Links und die Hälfte von SEVDESK_HTTP_FILES_MAX_MB; der Server höchstens 1.000 Datensätze und das volle Budget. Nichts wird verdrängt, sodass ein Konto die Dateien eines anderen nicht hinausdrängen kann; neue Anfragen werden abgelehnt, bis ältere ablaufen.

Logs und Audit. Logzeilen und die Audit-Zeilen file_received, file_link_created und file_downloaded tragen Konto, Zugang, Referenzcode, Art, Größe und SHA-256, nie IDs, Dateinamen oder Inhalt; Anfragezeilen zeigen die Routen-Vorlagen /uploads/* und /files/*, und ein upload:-Argument eines Werkzeugs wird als upload:[REDACTED] in das Audit-Log geschrieben.

Grenzen. Ein Agent, der einen angemeldeten Browser steuern kann, könnte selbst hoch- oder herunterladen, so wie er im Postfach freigeben könnte. Bei einer öffentlichen Loopback-URL über einfaches http erhalten andere lokale Webserver das Sitzungs-Cookie (der Start warnt). Wer TLS terminiert, sieht die Dateien unterwegs. Mit SEVDESK_OAUTH_ACCOUNTS=any hält der Server kurzzeitig Belege und Exporte anderer Leute (der Start warnt). Eine heruntergeladene Datei ist das, was sevDesk geliefert hat.

Lieferkette#

Releases werden aus der CI mit npm-Provenance veröffentlicht (Trusted Publishing, keine langlebigen Tokens), GitHub Actions sind per Commit-SHA festgelegt, und das Repository wird von CodeQL, Dependency Review und OpenSSF Scorecard geprüft.

Das Container-Image ghcr.io/jaylann/mcp-server-sevdesk wird vom Release-Workflow aus Basis-Images gebaut, die per Digest festgelegt sind. Es läuft als Benutzer ohne root-Rechte (uid 65532) auf einer distroless-Basis mit Node.js, und jedes Release trägt eine Build-Provenance-Attestierung sowie die SBOM und Provenance von BuildKit. Prüfen Sie ein Image, bevor Sie es betreiben, und legen Sie dann den geprüften Digest fest:

Terminal
gh attestation verify oci://ghcr.io/jaylann/mcp-server-sevdesk:<version> --owner jaylanndocker buildx imagetools inspect ghcr.io/jaylann/mcp-server-sevdesk:<version> --format '{{ json .SBOM }}'

Auch die Claude-Desktop-Erweiterung (mcp-server-sevdesk-<version>.mcpb) baut der Release-Workflow und hängt sie mit einer Build-Provenance-Attestierung an das GitHub-Release:

Terminal
gh attestation verify mcp-server-sevdesk-<version>.mcpb --owner jaylann