通俗地说: 这允许 AI 代理通过真实的 WordPress 应用程序密码——wp-admin 已经签发的同一种凭据类型——读取并轻度写入 WordPress 站点,除此之外别无其他。这里没有注册表单,没有 OAuth 客户端注册,没有任何形式的令牌端点。同一领域的一个插件恰恰发布了这些东西,而这正是一个未认证攻击者带着完整管理员权限全身而退的方式。
一个通过 REST API 暴露 MCP 服务器的 WordPress 插件,其安全模型是核心要点,而非功能清单上的一条。
CVE-2026-15015,CVSS 9.8:MountDev AI MCP Connector for WordPress 暴露了一个可公开访问的 OAuth 2.1 动态客户端注册端点,同时还有一个同样不检查请求者身份的授权端点。攻击者注册自己的 OAuth 客户端——无需任何凭据,该端点按设计就是未认证的,这正是“动态客户端注册”的含义——无人值守地走完授权端点流程,然后收到一个绑定到管理员账户的 bearer token。完全控制站点:内容、用户、设置。没有任何配置错误;该流程完全按照 OAuth 客户端注册应有的方式运行。只是它本就不该在没有任何管理员在场批准的情况下被访问到。
可推广的修复方案不是“更仔细地实现 OAuth”。而是根本不存在那个攻击面。这个插件不注册任何客户端注册端点、任何授权端点,以及任何形式的令牌签发端点。 攻击者没有任何东西可以注册,因为这里没有任何东西会分发凭据。认证使用的是管理员已经从 wp-admin 为特定用户创建的 WordPress 应用程序密码——这种凭据之所以存在,仅仅是因为一个拥有 manage_options 权限的人选择提前创建它,且完全在本插件之外、带外进行。
单独任何一层都不新鲜;有意地、彼此独立地同时运行两者,才能堵住任何一层出错时的漏洞:
1. 仅限应用程序密码——绝不用 cookie 会话。 WordPress 自带的 is_user_logged_in() 对于持有有效 cookie 和 nonce 的浏览器标签页同样返回 true。Auth::permission_callback() 有意拒绝这条路径:它监听 WordPress 核心在 Basic-auth 应用程序密码认证成功时专门触发的 application_password_did_authenticate 动作,并要求该标志,而不仅仅是“某个用户已登录”。MCP 客户端不是持有 nonce 的浏览器标签页;在这里接受 cookie 认证会打开一条 CSRF 形态的路径,通向一个可以用英语命令其修改内容的工具面,而相比这个插件真正需要的那扇门,这没有任何好处。
2. 按工具的能力检查,外加一个仅靠能力无法打开的写入闸门。 ToolRegistry::call() 在每次调用时重新检查 user_can($user_id, $capability)——文章用 edit_posts,订单用 manage_woocommerce。另外,唯一的写入工具 create_draft_post 还额外要求管理员从 设置 → WP Secure MCP 中选择启用,默认关闭。一个恰好带有 edit_posts 的应用程序密码,在有人拨动那个开关之前仍然无法写入任何内容——能力检查和站点级闸门是两把不同的锁,测试证明两者在任何方向上都不能相互替代。
limit,1–20)限制了单次调用能返回多少数据;一次合法但昂贵的 WP_Query 或 wc_get_orders 调用仍然要付出它应有的代价。每个工具的 tools/list 条目都带有 readOnlyHint / destructiveHint 注解,因此客户端可以决定是否在调用某个工具前询问人类,而无需事先知道它的功能。
composer install --no-dev
将插件目录复制(或符号链接)到 wp-content/plugins/,从 wp-admin 激活它,然后为代理应代表的账户创建一个应用程序密码:用户 → 你的个人资料 → 应用程序密码。
写入默认关闭。要允许 create_draft_post:设置 → WP Secure MCP → 写入工具。按工具的能力检查仍然在此基础上生效——拨动站点级开关不会授予任何人他们原本没有的能力。
55 个测试,全部为纯测试——无需 WordPress 安装、无需数据库、无需 Docker。 tests/bootstrap.php 以 WordPress 插件常规的单元测试方式(不加载 WordPress 本身)对 src/ 调用的 WordPress 函数(current_user_can、get_post、wp_insert_post 等)进行打桩。
composer install
vendor/bin/phpunit --testdox
最重的覆盖集中在最关键的地方:
ProtocolTest —— 针对假注册表的 17 个测试,完全不涉及 WordPress 特定内容。JSON-RPC 信封验证、每个错误码,以及 JSON-RPC 2.0 通知规则:没有 id 字段的请求不会得到响应,对任何方法都是如此,即使是格式错误的方法——错误无处可去。ToolRegistryTest —— 证明能力检查和写入闸门在两个方向上都是独立的:即使启用了写入,缺少能力也会阻止调用;即使具备能力,禁用的写入闸门也会阻止调用。AuthTest —— 这里最重要的测试是 test_logged_in_user_without_application_password_authentication_is_refused:一个真实、已解析的 WordPress 用户仍然被拒绝,因为从未请求过 cookie 会话。CI 在每次推送时对 PHP 8.1–8.3 运行 PHPUnit,并针对 WordPress Coding Standards 规则集运行 PHP_CodeSniffer。
CI 目前尚未在每次推送时运行的内容: 通过 @wordpress/env 进行的实时 WordPress + WooCommerce 集成测试——使用真实的应用程序密码对真实的 REST API 和真实的数据库进行认证,相当于 pg-readonly-mcp 的真实 Postgres CI 任务的实时实例版本。它已经编写并经过推敲,在 ci.yml 中作为 workflow_dispatch 任务接入,而非必需的关卡,因为本仓库自身的开发环境遇到了本地 Docker Desktop 故障,导致在此工作流首次真实运行之前无法演练 wp-env。在此说明,而不是留给别人去发现——这与 pg-readonly-mcp 对其自身未经测试的解析器差异缺口所适用的规则相同。
wp-secure-mcp.php 插件引导:加载自动加载器,接入一个 REST 路由
src/
Protocol.php JSON-RPC 2.0 分发。零 WordPress 调用。纯逻辑。
ToolRegistryInterface.php Protocol 与之对话的接缝,而非直接与 WordPress 对话
ToolRegistry.php 两把锁:按工具能力、站点级写入闸门
Auth.php 仅限应用程序密码的 permission_callback
Settings.php 写入闸门的管理员开关
RestController.php 轻量引导:一个路由,立即委托
Tools/ 四个 v1 工具
tests/
bootstrap.php WordPress 函数桩
ProtocolTest.php, ToolRegistryTest.php, AuthTest.php, ...
bin/smoke-test.sh 实时 wp-env 集成脚本(见上文测试部分)
如果某个请求被接受或拒绝的原因存在于 wp-secure-mcp.php 或 RestController.php 中,而不是 Auth、ToolRegistry 或 Protocol 中,那就是一个 bug——判断应该属于下一层,在那里可以用一个普通数组进行测试,别无其他。
MIT。
| 工具 | 能力 | 只读 | 备注 |
|---|
search_posts | edit_posts | 是 | 关键词搜索。硬编码为 post_status=publish——绝不返回草稿或私密文章,即使调用者能在 wp-admin 中看到它们。 |
get_post | edit_posts | 是 | 按 ID 获取。如果真实 ID 对应的真实文章状态不是 publish,则拒绝——ID 不是绕过 search_posts 所执行同一规则的途径。 |
list_recent_orders | manage_woocommerce | 是 | 仅状态、总额、货币、日期。绝不包含客户姓名、邮箱或地址——订单可见性不需要把客户的 PII 交给代理,无论是否通过能力检查。 |
create_draft_post | edit_posts | 否 | post_status 在源码中是字面量 'draft',不是参数。这个工具在任何输入下都无法发布,即使启用了写入。在管理员选择启用之前,站点范围内关闭。 |