
REST API를 통해 MCP 서버를 노출하는 WordPress 플러그인으로, 보안 모델이 핵심입니다. 해당 공격 표면을 아예 존재하지 않게 함으로써 CVE-2026-15015 OAuth 우회 형태를 차단합니다.
쉽게 말하면: 이 플러그인은 AI 에이전트가 실제 WordPress Application Password — wp-admin이 이미 발급하는 것과 동일한 자격 증명 유형 — 를 통해 WordPress 사이트를 읽고 가볍게 쓸 수 있게 해 주며, 그 외에는 아무것도 허용하지 않습니다. 여기에는 가입 양식도, OAuth 클라이언트 등록도, 어떤 종류의 토큰 엔드포인트도 없습니다. 같은 영역의 한 플러그인이 바로 그런 것을 제공했고, 그것이 인증되지 않은 공격자가 완전한 관리자 권한을 얻고 빠져나간 경로였습니다.
보안 모델 자체를 핵심으로 삼아 REST API 위에 MCP 서버를 노출하는 WordPress 플러그인입니다. 기능 목록의 한 항목이 아니라, 보안 모델이 요점입니다.
CVE-2026-15015, CVSS 9.8: WordPress용 MountDev AI MCP Connector는 공개적으로 접근 가능한 OAuth 2.1 Dynamic Client Registration 엔드포인트와, 누가 요청하는지 확인하지도 않는 authorization 엔드포인트를 함께 노출했습니다. 공격자는 자신의 OAuth 클라이언트를 등록하고 — 자격 증명이 필요 없습니다. 그 엔드포인트는 설계상 인증이 필요 없으며, 그것이 바로 "dynamic client registration"의 의미입니다 — authorization 엔드포인트를 사람의 개입 없이 통과시켜 관리자 계정에 묶인 bearer 토큰을 받습니다. 완전한 사이트 장악: 콘텐츠, 사용자, 설정. 잘못 구성된 것은 아무것도 없었습니다. 그 흐름은 OAuth 클라이언트 등록이 원래 작동해야 하는 방식대로 정확히 작동했습니다. 다만 이를 승인할 관리자가 이미 존재하지 않는 한 접근 가능해서는 안 되었을 뿐입니다.
일반화되는 수정은 "OAuth를 더 신중하게 구현하라"가 아닙니다. 그런 공격 표면을 아예 갖지 않는 것입니다. 이 플러그인은 클라이언트 등록 엔드포인트도, authorization 엔드포인트도, 어떤 종류의 토큰 발급 엔드포인트도 등록하지 않습니다. 공격자가 등록할 대상이 없습니다. 여기에는 자격 증명을 발급하는 것이 아무것도 없기 때문입니다. 인증은 관리자가 wp-admin에서 특정 사용자를 위해 이미 생성해 둔 WordPress Application Password입니다 — 이 자격 증명은 manage_options 권한을 가진 사람이 이 플러그인과 완전히 별개로, 미리, 대역 외에서 생성하기로 선택했기 때문에만 존재합니다.
어느 하나만으로도 새로운 것은 아닙니다. 둘 모두를, 의도적으로, 서로 독립적으로 운영하는 것이 어느 한쪽의 실수를 차단합니다:
1. Application Password만 — 쿠키 세션은 절대 안 됨. WordPress 자체의 is_user_logged_in()은 유효한 쿠키와 nonce를 가진 브라우저 탭에 대해서도 true입니다. Auth::permission_callback()은 그 경로를 의도적으로 거부합니다: WordPress 코어가 Basic-auth Application Password 인증이 성공했을 때만 발생시키는 application_password_did_authenticate 액션을 감지하고, "어떤 사용자가 로그인되어 있음"이 아니라 그 플래그를 요구합니다. MCP 클라이언트는 nonce를 가진 브라우저 탭이 아닙니다. 여기서 쿠키 인증을 허용하면, 영어로 콘텐츠를 수정하라고 지시할 수 있는 도구 표면으로 들어가는 CSRF 형태의 경로가 열리게 되며, 이 플러그인이 실제로 필요로 하는 단 하나의 문에 비해 아무런 이점도 없습니다.
2. 도구별 capability, 그리고 capability만으로는 열 수 없는 쓰기 게이트. ToolRegistry::call()은 매 호출마다 user_can($user_id, $capability)를 새로 확인합니다 — 게시물에는 edit_posts, 주문에는 manage_woocommerce. 별도로, 유일한 쓰기 도구인 create_draft_post는 추가로 관리자가 Settings → WP Secure MCP에서 옵트인했을 것을 요구하며, 기본값은 꺼짐입니다. 우연히 edit_posts를 가진 Application Password라도 사람이 그 스위치를 켜기 전까지는 아무것도 쓸 수 없습니다 — capability 검사와 사이트 전역 게이트는 서로 다른 두 개의 잠금장치이며, 테스트는 어느 방향으로도 어느 하나가 다른 하나를 대체하지 못함을 증명합니다.
limit, 1–20)은 단일 호출이 반환할 수 있는 양을 제한합니다. 정당하게 비용이 많이 드는 WP_Query나 wc_get_orders 호출은 여전히 그만큼의 비용이 듭니다.모든 도구의 tools/list 항목은 readOnlyHint / destructiveHint 주석을 포함하므로, 클라이언트는 도구가 무엇을 하는지 미리 알 필요 없이 호출 전에 사람에게 물어볼지 결정할 수 있습니다.
composer install --no-dev
플러그인 디렉터리를 wp-content/plugins/로 복사(또는 심볼릭 링크)하고, wp-admin에서 활성화한 다음, 에이전트가 행동할 계정에 대한 Application Password를 생성하세요: Users → your profile → Application Passwords.
쓰기는 기본적으로 꺼져 있습니다. create_draft_post를 허용하려면: Settings → WP Secure MCP → Write tools. 도구별 capability 검사는 이 위에 여전히 적용됩니다 — 사이트 전역 스위치를 켠다고 해서 누군가에게 이미 가지고 있지 않은 capability를 부여하지는 않습니다.
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 봉투 검증, 모든 오류 코드, 그리고 id 필드가 없는 요청은 어떤 메서드에 대해서도, 심지어 잘못된 형식의 요청이라도 응답을 받지 못한다는 JSON-RPC 2.0 알림 규칙 — 오류가 갈 곳이 없음.ToolRegistryTest — capability 검사와 쓰기 게이트가 양방향으로 독립적임을 증명: 쓰기가 활성화된 상태에서도 누락된 capability는 호출을 차단하고, capability가 존재하는 상태에서도 비활성화된 쓰기 게이트는 호출을 차단함.AuthTest — 여기서 가장 중요한 테스트는 test_logged_in_user_without_application_password_authentication_is_refused임: 실제로 확인된 WordPress 사용자라도 거부되는데, 쿠키 세션은 애초에 요청되지 않았기 때문임.CI는 모든 푸시에서 PHP 8.1–8.3에 걸쳐 PHPUnit을, 그리고 WordPress Coding Standards 규칙 세트에 대해 PHP_CodeSniffer를 실행합니다.
CI가 아직 모든 푸시에서 실행하지 않는 것: @wordpress/env를 통한 실제 WordPress + WooCommerce 통합 테스트 — 실제 Application Password로 실제 REST API와 실제 데이터베이스에 인증하는, pg-readonly-mcp의 실제 Postgres CI 작업에 해당하는 라이브 인스턴스 버전입니다. 이는 작성되고 검토되었으며, ci.yml에 workflow_dispatch 작업으로 연결되어 있지만 필수 게이트는 아닙니다. 이 저장소 자체의 개발 환경에서 로컬 Docker Desktop 장애가 발생하여 이 워크플로의 첫 실제 실행 전에 wp-env를 예행연습하는 것이 불가능했기 때문입니다. 다른 누군가가 발견하도록 남겨두지 않고 여기에 적어 둡니다 — 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)
요청이 Auth, ToolRegistry, 또는 Protocol이 아니라 wp-secure-mcp.php나 RestController.php에 있는 이유로 수락되거나 거부된다면, 그것은 버그입니다 — 판단은 한 계층 아래, 순수한 배열만으로 테스트할 수 있는 곳에 속합니다.
MIT.
| 도구 | Capability | 읽기 전용 | 비고 |
|---|
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를 넘겨줄 것을 요구하지는 않으며, capability 검사 여부와 무관함. |
create_draft_post | edit_posts | 아니오 | post_status는 소스에서 리터럴 'draft'이며, 인자가 아님. 이 도구는 어떤 입력으로도, 쓰기가 활성화된 상태에서도 발행할 수 없음. 관리자가 옵트인하기 전까지 사이트 전역에서 꺼져 있음. |