
Ein WordPress-Plugin, das einen MCP-Server über die REST-API bereitstellt, wobei das Sicherheitsmodell im Mittelpunkt steht -- es schließt die OAuth-Bypass-Form von CVE-2026-15015, indem diese Angriffsfläche gar nicht erst existiert.
Einfach ausgedrückt: Dies ermöglicht einem KI-Agenten, eine WordPress-Website über ein echtes WordPress-Anwendungspasswort zu lesen und in begrenztem Umfang zu beschreiben — denselben Anmeldedatentyp, den wp-admin bereits ausstellt — und nichts anderes. Es gibt hier kein Registrierungsformular, keine OAuth-Client-Registrierung, keinen Token-Endpunkt jeglicher Art. Ein Plugin in diesem Bereich hat genau das ausgeliefert, und so ist ein nicht authentifizierter Angreifer mit vollem Administratorzugriff davongekommen.
Ein WordPress-Plugin, das einen MCP-Server über die REST-API bereitstellt, wobei das Sicherheitsmodell der Kern ist, nicht ein Feature-Aufzählungspunkt.
CVE-2026-15015, CVSS 9.8: Der MountDev AI MCP Connector für WordPress hat einen öffentlich zugänglichen OAuth-2.1-Dynamic-Client-Registration-Endpunkt zusammen mit einem Autorisierungsendpunkt bereitgestellt, der ebenfalls nicht überprüfte, wer fragte. Ein Angreifer registriert seinen eigenen OAuth-Client — keine Anmeldedaten erforderlich, der Endpunkt ist per Design nicht authentifiziert, das bedeutet „dynamische Client-Registrierung" — führt ihn unbeaufsichtigt durch den Autorisierungsendpunkt und erhält ein Bearer-Token, das an ein Administratorkonto gebunden ist. Volle Kontrolle über die Website: Inhalte, Benutzer, Einstellungen. Nichts war falsch konfiguriert; der Ablauf funktionierte genau so, wie OAuth-Client-Registrierung funktionieren soll. Er hätte nur niemals erreichbar sein dürfen, ohne dass bereits ein Administrator anwesend ist, um ihn zu genehmigen.
Die Verallgemeinerung der Lösung ist nicht „OAuth sorgfältiger
implementieren". Es ist, diese Angriffsfläche gar nicht erst zu haben.
Dieses Plugin registriert keinen Client-Registrierungsendpunkt, keinen
Autorisierungsendpunkt und keinen Token-Ausstellungsendpunkt jeglicher Art.
Es gibt nichts, wogegen ein Angreifer sich registrieren könnte, weil es hier
nichts gibt, das Anmeldedaten ausgibt. Die Authentifizierung erfolgt über ein
WordPress-Anwendungspasswort, das ein Administrator bereits für einen
bestimmten Benutzer über wp-admin erstellt hat — ein Anmeldedatensatz, der
nur existiert, weil ein Mensch mit manage_options sich entschieden hat, ihn
im Voraus und außerhalb dieses Plugins zu erstellen.
Keine allein ist neu; beide absichtlich, unabhängig voneinander laufen zu lassen, ist das, was einen Fehler in einer der beiden ausschließt:
1. Nur Anwendungspasswort — niemals eine Cookie-Sitzung. WordPress'
eigenes is_user_logged_in() ist auch für einen Browser-Tab wahr, der ein
gültiges Cookie und Nonce hält. Auth::permission_callback()
lehnt diesen Pfad bewusst ab: Es lauscht auf die Aktion
application_password_did_authenticate, die der WordPress-Kern speziell
dann auslöst, wenn die Basic-Auth-Anwendungspasswort-Authentifizierung
erfolgreich ist, und verlangt dieses Flag, nicht nur „irgendein Benutzer ist
angemeldet". Ein MCP-Client ist kein Browser-Tab mit einem Nonce; Cookie-Auth
hier zu akzeptieren würde einen CSRF-förmigen Pfad in eine Tool-Oberfläche
öffnen, der man auf Englisch sagen kann, sie solle Inhalte ändern — ohne
Nutzen gegenüber der einen Tür, die dieses Plugin tatsächlich braucht.
2. Pro-Tool-Berechtigung, plus ein Schreib-Gate, das die Berechtigung
allein nicht öffnen kann. ToolRegistry::call()
prüft user_can($user_id, $capability) bei jedem Aufruf frisch —
edit_posts für Beiträge, manage_woocommerce für Bestellungen. Separat
verlangt das einzige Schreib-Tool, create_draft_post, zusätzlich, dass ein
Administrator über Einstellungen → WP Secure MCP zugestimmt hat,
standardmäßig deaktiviert. Ein Anwendungspasswort, das zufällig edit_posts
trägt, kann trotzdem nichts schreiben, bis ein Mensch diesen Schalter
umgelegt hat — eine Berechtigungsprüfung und ein websiteweites Gate sind
zwei verschiedene Schlösser, und die Tests beweisen, dass keines das andere
in irgendeine Richtung ersetzt.
limit, 1–20) begrenzen, wie viel ein einzelner Aufruf zurückgeben kann;
ein legitim teurer WP_Query- oder wc_get_orders-Aufruf kostet trotzdem,
was er kostet.Der tools/list-Eintrag jedes Tools trägt readOnlyHint- /
destructiveHint-Annotationen, sodass ein Client entscheiden kann, ob er
einen Menschen fragen soll, bevor er eines aufruft, ohne bereits wissen zu
müssen, was es tut.
composer install --no-dev
Kopiere (oder verlinke) das Plugin-Verzeichnis nach wp-content/plugins/,
aktiviere es über wp-admin und erstelle dann ein Anwendungspasswort für das
Konto, als das ein Agent handeln soll: Benutzer → dein Profil →
Anwendungspasswörter.
Schreibvorgänge sind standardmäßig aus. Um create_draft_post zu erlauben:
Einstellungen → WP Secure MCP → Schreib-Tools. Die
Pro-Tool-Berechtigungsprüfung gilt weiterhin zusätzlich — das Umlegen des
websiteweiten Schalters gewährt niemandem eine Berechtigung, die er nicht
bereits hatte.
55 Tests, alle rein — keine WordPress-Installation, keine Datenbank, kein
Docker. tests/bootstrap.php stubbt die
WordPress-Funktionen, die src/ aufruft (current_user_can, get_post,
wp_insert_post, ...), so wie ein WordPress-Plugin konventionell
unittestet wird, ohne WordPress selbst zu laden.
composer install
vendor/bin/phpunit --testdox
Die schwerste Abdeckung liegt dort, wo es am meisten zählt:
ProtocolTest — 17 Tests gegen eine gefälschte
Registry, nichts WordPress-Spezifisches überhaupt. JSON-RPC-Envelope-
Validierung, jeder Fehlercode und die JSON-RPC-2.0-Benachrichtigungsregel,
dass eine Anfrage ohne id-Feld keine Antwort erhält, für jede Methode,
selbst eine fehlerhafte — es gibt keinen Ort, an den der Fehler gehen
könnte.ToolRegistryTest — beweist, dass die
Berechtigungsprüfung und das Schreib-Gate in beide Richtungen unabhängig
sind: eine fehlende Berechtigung blockiert einen Aufruf, selbst wenn
Schreibvorgänge aktiviert sind, und ein deaktiviertes Schreib-Gate
blockiert einen Aufruf, selbst wenn die Berechtigung vorhanden ist.AuthTest — der Test, der hier am meisten zählt,
ist test_logged_in_user_without_application_password_authentication_is_refused:
ein echter, aufgelöster WordPress-Benutzer wird trotzdem abgelehnt, weil
eine Cookie-Sitzung nie verlangt wurde.CI führt PHPUnit über PHP 8.1–8.3 und PHP_CodeSniffer gegen das WordPress Coding Standards-Regelset bei jedem Push aus.
Was CI noch nicht bei jedem Push ausführt: einen Live-WordPress- +
WooCommerce-Integrationstest über
@wordpress/env — Authentifizierung mit einem echten
Anwendungspasswort gegen eine echte REST-API und eine echte Datenbank, das
Live-Instanz-Äquivalent zum echten Postgres-CI-Job von
pg-readonly-mcp. Er ist
geschrieben und durchdacht, als workflow_dispatch-Job in
ci.yml verdrahtet statt als erforderliches
Gate, weil die eigene Entwicklungsumgebung dieses Repositorys auf einen
lokalen Docker-Desktop-Fehler stieß, der es unmöglich machte, wp-env vor
dem ersten echten Lauf dieses Workflows zu proben. Hier gesagt, statt es
jemand anderem zu überlassen, es zu entdecken — dieselbe Regel, die
pg-readonly-mcp auf seine eigene ungetestete Parser-Differential-Lücke
anwendet.
wp-secure-mcp.php plugin bootstrap: loads the autoloader, wires one REST route
src/
Protocol.php JSON-RPC 2.0 dispatch. Zero WordPress calls. Pure.
ToolRegistryInterface.php the seam Protocol talks to instead of WordPress directly
ToolRegistry.php the two locks: per-tool capability, server-wide write gate
Auth.php Application-Password-only permission_callback
Settings.php the write-gate admin toggle
RestController.php thin bootstrap: one route, delegates immediately
Tools/ the four v1 tools
tests/
bootstrap.php WordPress function stubs
ProtocolTest.php, ToolRegistryTest.php, AuthTest.php, ...
bin/smoke-test.sh live wp-env integration script (see Tests, above)
Wenn eine Anfrage jemals aus einem Grund akzeptiert oder abgelehnt wird, der
in wp-secure-mcp.php oder RestController.php statt in Auth,
ToolRegistry oder Protocol liegt, ist das ein Bug — das Urteil gehört
eine Schicht tiefer, wo es mit einem einfachen Array und nichts anderem
getestet werden kann.
MIT.
| Tool | Berechtigung | Nur lesend | Hinweise |
|---|
search_posts | edit_posts | Ja | Stichwortsuche. Fest auf post_status=publish verdrahtet — gibt niemals Entwürfe oder private Beiträge zurück, selbst an einen Aufrufer, der sie in wp-admin sehen könnte. |
get_post | edit_posts | Ja | Abruf per ID. Verweigert einen echten Beitrag an einer echten ID, wenn sein Status nicht publish ist — eine ID ist keine Umgehung derselben Regel, die search_posts erzwingt. |
list_recent_orders | manage_woocommerce | Ja | Nur Status, Gesamtbetrag, Währung, Datum. Niemals Kundenname, E-Mail oder Adresse — Bestellsichtbarkeit erfordert nicht, einem Agenten die PII eines Kunden auszuhändigen, Berechtigungsprüfung hin oder her. |
create_draft_post | edit_posts | Nein | post_status ist ein literales 'draft' in der Quelle, kein Argument. Dieses Tool kann unter keiner Eingabe veröffentlichen, selbst mit aktivierten Schreibvorgängen. Websiteweit aus, bis ein Administrator zustimmt. |