
Um plugin WordPress que expõe um servidor MCP sobre a REST API, com o modelo de segurança como ponto central -- fecha o formato de bypass de OAuth da CVE-2026-15015 ao nunca ter essa superfície.
Em termos simples: isto permite que um agente de IA leia e escreva ligeiramente num site WordPress através de uma Application Password real do WordPress — o mesmo tipo de credencial que o wp-admin já emite — e nada mais. Não há formulário de registo, nem registo de cliente OAuth, nem endpoint de token de qualquer tipo aqui. Um plugin neste mesmo espaço disponibilizou exatamente isso, e foi assim que um atacante não autenticado saiu com acesso total de administrador.
Um plugin WordPress que expõe um servidor MCP sobre a REST API, com o modelo de segurança como ponto central, não como um item de destaque.
CVE-2026-15015, CVSS 9.8: o MountDev AI MCP Connector para WordPress expunha um endpoint de Registo Dinâmico de Cliente OAuth 2.1 publicamente acessível, juntamente com um endpoint de autorização que também não verificava quem estava a pedir. Um atacante regista o seu próprio cliente OAuth — sem credenciais necessárias, o endpoint é não autenticado por design, é isso que "registo dinâmico de cliente" significa — percorre-o através do endpoint de autorização sem supervisão, e recebe um bearer token associado a uma conta de administrador. Controlo total do site: conteúdo, utilizadores, definições. Nada estava mal configurado; o fluxo funcionou exatamente como o registo de cliente OAuth deve funcionar. Simplesmente nunca deveria ter sido acessível sem um administrador já presente para o aprovar.
A correção que se generaliza não é "implementar OAuth com mais cuidado". É
não ter essa superfície. Este plugin não regista nenhum endpoint de
registo de cliente, nenhum endpoint de autorização, e nenhum endpoint de
emissão de tokens de qualquer tipo. Não há nada contra o que um atacante
se possa registar, porque não há aqui nada que distribua credenciais. A
autenticação é uma Application Password do WordPress que um administrador já
criou para um utilizador específico a partir do wp-admin — uma credencial que
existe apenas porque um humano com manage_options escolheu criá-la,
antecipadamente, fora de banda deste plugin por completo.
Nenhuma delas sozinha é nova; executar ambas, de propósito, independentes uma da outra, é o que fecha um erro em qualquer uma delas:
1. Apenas Application Password — nunca uma sessão de cookie. O próprio
is_user_logged_in() do WordPress também é verdadeiro para um separador de
navegador que tenha um cookie e nonce válidos.
Auth::permission_callback() recusa esse caminho
deliberadamente: escuta a ação application_password_did_authenticate que o
núcleo do WordPress dispara especificamente quando a autenticação Basic-auth
por Application Password tem sucesso, e exige esse sinalizador, não apenas
"algum utilizador está autenticado". Um cliente MCP não é um separador de
navegador com um nonce; aceitar autenticação por cookie aqui abriria um
caminho em forma de CSRF para uma superfície de ferramentas que pode receber,
em inglês, a instrução de modificar conteúdo, sem qualquer benefício em
relação à única porta de que este plugin realmente precisa.
2. Capacidade por ferramenta, mais um portão de escrita que a capacidade
sozinha não consegue abrir. ToolRegistry::call()
verifica user_can($user_id, $capability) de novo em cada chamada —
edit_posts para posts, manage_woocommerce para encomendas. Separadamente,
a única ferramenta de escrita, create_draft_post, exige adicionalmente que
um administrador tenha optado por ativá-la em Settings → WP Secure MCP,
desativada por predefinição. Uma Application Password que por acaso tenha
edit_posts continua a não poder escrever nada até um humano ter ativado
esse interruptor — uma verificação de capacidade e um portão ao nível do
site são dois fechaduras diferentes, e os testes provam que nenhuma
substitui a outra em qualquer direção.
limit, 1–20) limitam quanto uma única chamada pode devolver; uma chamada
WP_Query ou wc_get_orders legitimamente dispendiosa continua a custar o
que custa.| Ferramenta | Capacidade | Apenas leitura | Notas |
|---|---|---|---|
search_posts | edit_posts | Sim | Pesquisa por palavra-chave. Fixada em post_status=publish — nunca devolve rascunhos ou posts privados, mesmo a um chamador que os pudesse ver no wp-admin. |
get_post | edit_posts | Sim | Obter por ID. Recusa um post real num ID real se o seu estado não for publish — um ID não é um bypass à mesma regra que search_posts impõe. |
list_recent_orders | manage_woocommerce | Sim | Apenas estado, total, moeda, data. Nunca nome, email ou morada do cliente — a visibilidade de encomendas não exige entregar a um agente os dados pessoais de um cliente, com ou sem verificação de capacidade. |
create_draft_post | edit_posts | Não | post_status é um literal 'draft' em o código-fonte, não um argumento. Esta ferramenta não pode publicar com nenhuma entrada, mesmo com as escritas ativadas. Desativada ao nível do site até um administrador optar por ativá-la. |
A entrada tools/list de cada ferramenta inclui anotações readOnlyHint /
destructiveHint, para que um cliente possa decidir se deve perguntar a um
humano antes de chamar uma, sem precisar de já saber o que ela faz.
composer install --no-dev
Copie (ou crie um link simbólico para) o diretório do plugin para
wp-content/plugins/, ative-o a partir do wp-admin, e depois crie uma
Application Password para a conta em nome da qual o agente deve atuar:
Users → o seu perfil → Application Passwords.
As escritas estão desativadas por predefinição. Para permitir
create_draft_post: Settings → WP Secure MCP → Write tools. A
verificação de capacidade por ferramenta continua a aplicar-se por cima
disto — ativar o interruptor ao nível do site não concede a ninguém uma
capacidade que já não tivesse.
55 testes, todos puros — sem instalação do WordPress, sem base de dados,
sem Docker. tests/bootstrap.php cria stubs das
funções do WordPress que src/ chama (current_user_can, get_post,
wp_insert_post, ...) da mesma forma que um plugin WordPress é
convencionalmente testado unitariamente sem carregar o próprio WordPress.
composer install
vendor/bin/phpunit --testdox
A cobertura mais pesada está onde mais importa: