Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
wp-secure-mcp — A WordPress plugin exposing an MCP server over the REST API, with the security model as the point -- closes the CVE-2026-15015 OAuth-bypass shape by never having that surface at all. | Kitploit
Tools/GitHubGitHub/les-k/wp-secure-mcp
Authentication & AuthorizationDefensive ToolsVulnerability AnalysisWeb SecurityUtilities & FrameworksIdentity & Access Management (IAM)API SecurityAI Security
GitHubles-k/wp-secure-mcp

wp-secure-mcp

A WordPress plugin exposing an MCP server over the REST API, with the security model as the point -- closes the CVE-2026-15015 OAuth-bypass shape by never having that surface at all.

16h 49m agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
View Repository

wp-secure-mcp

CI PHP License: MIT

In plain terms: this lets an AI agent read and lightly write to a WordPress site through a real WordPress Application Password — the same credential type wp-admin already issues — and nothing else. There is no sign-up form, no OAuth client registration, no token endpoint of any kind here. A plugin in this same space shipped exactly that, and it is how an unauthenticated attacker walked out with full admin access.

A WordPress plugin exposing an MCP server over the REST API, with the security model as the point, not a feature bullet.

The bypass this exists to close

CVE-2026-15015, CVSS 9.8: the MountDev AI MCP Connector for WordPress exposed a publicly accessible OAuth 2.1 Dynamic Client Registration endpoint alongside an authorization endpoint that didn't check who was asking either. An attacker registers their own OAuth client — no credentials required, the endpoint is unauthenticated by design, that's what "dynamic client registration" means — walks it through the authorization endpoint unattended, and receives a bearer token bound to an administrator account. Full site control: content, users, settings. Nothing was misconfigured; the flow worked exactly as OAuth client registration is supposed to. It just should never have been reachable without an admin already being present to approve it.

The fix that generalises isn't "implement OAuth more carefully." It's not having that surface. This plugin registers no client-registration endpoint, no authorization endpoint, and no token-issuance endpoint of any kind. There is nothing for an attacker to register against, because there is nothing here that hands out credentials. Authentication is a WordPress Application Password an administrator already created for a specific user from wp-admin — a credential that exists only because a human with manage_options chose to create it, ahead of time, out of band from this plugin entirely.

Two independent layers

Neither one alone is new; running both, on purpose, independent of each other, is what closes off a mistake in either one:

1. Application Password only — never a cookie session. WordPress's own is_user_logged_in() is also true for a browser tab holding a valid cookie and nonce. Auth::permission_callback() refuses that path deliberately: it listens for the application_password_did_authenticate action WordPress core fires specifically when Basic-auth Application Password authentication succeeds, and requires that flag, not just "some user is logged in." An MCP client is not a browser tab with a nonce; accepting cookie auth here would open a CSRF-shaped path into a tool surface that can be told, in English, to modify content, for no benefit over the one door this plugin actually needs.

2. Per-tool capability, plus a write gate that capability alone can't open. ToolRegistry::call() checks user_can($user_id, $capability) fresh on every call — edit_posts for posts, manage_woocommerce for orders. Separately, the one write tool, create_draft_post, additionally requires an administrator to have opted in from Settings → WP Secure MCP, off by default. An Application Password that happens to carry edit_posts still cannot write anything until a human has flipped that switch — a capability check and a site-wide gate are two different locks, and the tests prove neither one substitutes for the other in either direction.

What it does not protect against

  • A site owner who issues an Application Password to a user with more capability than the task needs. This plugin enforces the capability model WordPress already has; it does not second-guess who an administrator decided to trust.
  • Resource exhaustion within the limits. Row caps (limit, 1–20) bound how much a single call can return; a legitimately expensive WP_Query or wc_get_orders call still costs what it costs.
  • What the agent does with data once it has it. This is an access gate at the WordPress boundary, not a data-loss-prevention tool.
  • A vulnerability in WordPress, WooCommerce, or PHP itself. The two layers above are this plugin's own contribution; they sit on top of WordPress's capability system and Application Password implementation, and inherit whatever either of those gets wrong.

Tools

Every tool's tools/list entry carries readOnlyHint / destructiveHint annotations, so a client can decide whether to ask a human before calling one without needing to already know what it does.

Install

root@kitploit:~
composer install --no-dev

Copy (or symlink) the plugin directory into wp-content/plugins/, activate it from wp-admin, then create an Application Password for the account an agent should act as: Users → your profile → Application Passwords.

Configure

Writes are off by default. To allow create_draft_post: Settings → WP Secure MCP → Write tools. The per-tool capability check still applies on top of this — flipping the site-wide switch does not grant anyone a capability they didn't already have.

Tests

55 tests, all pure — no WordPress install, no database, no Docker. tests/bootstrap.php stubs the WordPress functions src/ calls (current_user_can, get_post, wp_insert_post, ...) the same way a WordPress plugin is conventionally unit-tested without loading WordPress itself.

root@kitploit:~
composer install
vendor/bin/phpunit --testdox

The heaviest coverage sits where it matters most:

  • ProtocolTest — 17 tests against a fake registry, nothing WordPress-specific at all. JSON-RPC envelope validation, every error code, and the JSON-RPC 2.0 notification rule that a request with no id field gets no response, for any method, even a malformed one — there's nowhere for the error to go.
  • ToolRegistryTest — proves the capability check and the write gate are independent in both directions: a missing capability blocks a call even when writes are enabled, and a disabled write gate blocks a call even when the capability is present.
  • AuthTest — the test that matters most here is test_logged_in_user_without_application_password_authentication_is_refused: a real, resolved WordPress user is still refused, because a cookie session was never asked for.

CI runs PHPUnit across PHP 8.1–8.3 and PHP_CodeSniffer against the WordPress Coding Standards ruleset on every push.

What CI does not yet run on every push: a live WordPress + WooCommerce integration test via @wordpress/env — authenticate with a real Application Password against a real REST API and a real database, the live-instance equivalent of pg-readonly-mcp's real-Postgres CI job. It's written and reasoned through, wired as a workflow_dispatch job in ci.yml rather than a required gate, because this repository's own development environment hit a local Docker Desktop failure that made it impossible to rehearse wp-env before this workflow's first real run. Said here rather than left for someone else to discover — the same rule pg-readonly-mcp applies to its own untested parser-differential gap.

Layout

root@kitploit:~
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)

If a request is ever accepted or refused for a reason that lives in wp-secure-mcp.php or RestController.php instead of Auth, ToolRegistry, or Protocol, that's a bug — the judgement belongs one layer down, where it can be tested with a plain array and nothing else.

Licence

MIT.

Download Tool
ToolCapabilityRead-onlyNotes
search_postsedit_postsYesKeyword search. Hardcoded to post_status=publish — never returns drafts or private posts, even to a caller who could see them in wp-admin.
get_postedit_postsYesFetch by ID. Refuses a real post at a real ID if its status isn't publish — an ID is not a bypass around the same rule search_posts enforces.
list_recent_ordersmanage_woocommerceYesStatus, total, currency, date only. Never customer name, email, or address — order visibility doesn't require handing an agent a customer's PII, capability check or not.
create_draft_postedit_postsNopost_status is a literal 'draft' in the source, not an argument. This tool cannot publish under any input, even with writes enabled. Off site-wide until an administrator opts in.