
Плагин WordPress, предоставляющий MCP-сервер через REST API, где главное — модель безопасности: он закрывает вектор обхода OAuth из CVE-2026-15015 за счёт того, что такой поверхности там просто нет.
Простыми словами: это позволяет ИИ-агенту читать и немного записывать данные на сайт WordPress через настоящий Application Password WordPress — тот же тип учётных данных, который уже выдаёт wp-admin — и ничего больше. Здесь нет формы регистрации, нет регистрации OAuth-клиента, нет никакого эндпоинта выдачи токенов. Плагин в этой же нише выпустил ровно это, и именно так неаутентифицированный атакующий получил полный административный доступ.
Плагин WordPress, предоставляющий MCP-сервер через REST API, где модель безопасности — это суть, а не пункт в списке возможностей.
CVE-2026-15015, CVSS 9.8: MountDev AI MCP Connector for WordPress предоставлял публично доступный эндпоинт OAuth 2.1 Dynamic Client Registration вместе с эндпоинтом авторизации, который тоже не проверял, кто спрашивает. Атакующий регистрирует свой собственный OAuth-клиент — учётные данные не требуются, эндпоинт неаутентифицирован по замыслу, это и означает «dynamic client registration» — проводит его через эндпоинт авторизации без участия человека и получает bearer-токен, привязанный к учётной записи администратора. Полный контроль над сайтом: контент, пользователи, настройки. Ничего не было настроено неправильно; поток работал ровно так, как и должен работать OAuth client registration. Просто он никогда не должен был быть доступен без уже присутствующего администратора для его одобрения.
Обобщающее исправление — не «реализовать OAuth более аккуратно». Это
отсутствие такой поверхности. Этот плагин не регистрирует ни эндпоинт
регистрации клиентов, ни эндпоинт авторизации, ни эндпоинт выдачи токенов
какого-либо рода. Атакующему не с чем регистрироваться, потому что
здесь нет ничего, что выдаёт учётные данные. Аутентификация — это WordPress
Application Password, который администратор уже создал для конкретного пользователя
из wp-admin — учётные данные, которые существуют только потому, что человек с
manage_options решил их создать, заранее, вне этого
плагина полностью.
Ни один из них по отдельности не нов; работа обоих, намеренно, независимо друг от друга, — вот что закрывает ошибку в любом из них:
1. Только Application Password — никогда cookie-сессия. Собственный
is_user_logged_in() WordPress также истинен для вкладки браузера, держащей валидный
cookie и nonce. Auth::permission_callback() намеренно
отказывает этому пути: он слушает действие application_password_did_authenticate,
которое ядро WordPress вызывает специально, когда Basic-auth Application
Password аутентификация успешна, и требует этот флаг, а не просто «какой-то
пользователь вошёл в систему». MCP-клиент — это не вкладка браузера с nonce;
принятие здесь cookie-аутентификации открыло бы путь в форме CSRF к поверхности
инструментов, которой можно на естественном языке приказать
изменять контент, без какой-либо выгоды по сравнению с
единственной дверью, которая этому плагину действительно нужна.
2. Право на инструмент, плюс шлюз записи, который одно право открыть не
может. ToolRegistry::call() проверяет
user_can($user_id, $capability) заново при каждом вызове — edit_posts для
записей, manage_woocommerce для заказов. Отдельно единственный инструмент
записи, create_draft_post, дополнительно требует, чтобы администратор
включил его в Settings → WP Secure MCP, по умолчанию выключено. Application Password,
который случайно несёт edit_posts, всё равно не может ничего записать, пока человек
не переключил этот тумблер — проверка права и общесайтовый шлюз — это два
разных замка, и тесты доказывают, что ни один из них не заменяет
другой ни в одном из направлений.
limit, 1–20) ограничивают
то, сколько может вернуть один вызов; законно дорогой вызов WP_Query или
wc_get_orders всё равно стоит столько, сколько стоит.Запись каждого инструмента в tools/list несёт аннотации readOnlyHint / destructiveHint,
чтобы клиент мог решить, спрашивать ли человека перед вызовом,
не зная заранее, что этот инструмент делает.
composer install --no-dev
Скопируйте (или создайте символическую ссылку) каталог плагина в wp-content/plugins/, активируйте
его из wp-admin, затем создайте Application Password для учётной записи, от имени которой
должен действовать агент: Users → ваш профиль → Application Passwords.
Записи по умолчанию выключены. Чтобы разрешить create_draft_post: Settings → WP
Secure MCP → Write tools. Проверка права на инструмент всё равно применяется
поверх этого — переключение общесайтового тумблера не даёт никому
права, которого у него и так не было.
55 тестов, все чистые — без установки WordPress, без базы данных, без Docker.
tests/bootstrap.php заглушает функции WordPress, которые
вызывает src/ (current_user_can, get_post, wp_insert_post, ...), так же,
как плагин WordPress обычно юнит-тестируется без загрузки самого
WordPress.
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 запускает PHPUnit на PHP 8.1–8.3 и PHP_CodeSniffer против набора правил WordPress Coding Standards при каждом push.
Что CI пока не запускает при каждом push: живой интеграционный тест
WordPress + WooCommerce через
@wordpress/env — аутентификация с настоящим Application
Password против настоящего REST API и настоящей базы данных, живой эквивалент
CI-задачи с настоящим Postgres у pg-readonly-mcp.
Он написан и продуман, подключён как задача workflow_dispatch в ci.yml,
а не как обязательный гейт, потому что собственная среда разработки этого репозитория столкнулась
с локальным сбоем Docker Desktop, который сделал невозможным отрепетировать wp-env
перед первым реальным запуском этого workflow. Сказано здесь, а не оставлено
кому-то другому для обнаружения — то же правило, которое pg-readonly-mcp применяет к своему
собственному непротестированному пробелу в дифференциале парсеров.
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)
Если запрос когда-либо принимается или отклоняется по причине, которая живёт в
wp-secure-mcp.php или RestController.php, а не в Auth, ToolRegistry
или Protocol, это баг — суждение принадлежит слоем ниже, где его
можно протестировать с помощью обычного массива и ничего больше.
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 | Да | Только статус, сумма, валюта, дата. Никогда имя клиента, email или адрес — видимость заказа не требует передачи агенту PII клиента, независимо от проверки прав. |
create_draft_post | edit_posts | Нет | post_status — это литерал 'draft' в исходном коде, а не аргумент. Этот инструмент не может опубликовать ни при каком вводе, даже при включённых записях. Выключен на уровне всего сайта, пока администратор не включит его. |