Repository navigation
Security Model
A key delegates an existing WordPress identity and may only reduce what that identity can do. The MCP route is private, the management API is separate, and every tool call is re-evaluated against live permissions, the key scope and site policy (docs/en/adr/0009-origin-and-auth-route.md, docs/en/adr/0005-capability-intersection.md).
| Surface | Route | Accepted identity | Rejected identity |
|---|---|---|---|
| MCP |
/xnetvn-agent-hub-wp/mcp only |
Bearer key in an approved header | Cookie, Application Password, query-string or body credential |
| Management | /xnetvn-agent-hub-wp/v1/* |
Logged-in auth cookie plus wp_rest nonce |
Bearer key, Application Password |
| STDIO | Local adapter process | Explicitly selected WordPress user | No HTTP path exists |
The MCP route matcher compares the normalized route exactly and only inside a real REST request; a namespace prefix or a different route is not authenticated (src/Http/RouteMatcher.php). On that route the bearer header is the only credential source; the fallback header is configurable and enabled by default (src/Auth/HeaderExtractor.php). Cookie identity is not a substitute: the authentication filter replaces the resolved user with the key owner and re-runs late enough to override an early-cached cookie user while preserving earlier guard errors (src/Auth/Authenticator.php).
Management calls require a real logged_in auth cookie for the current user and a valid wp_rest nonce. A present but malformed nonce header is authoritative and does not fall back to a query parameter (src/Rest/ManagementGate.php). Every management controller inherits this gate, so no route can skip it (src/Rest/ManagementController.php).
A successful MCP authentication creates a request-local AuthContext binding the key metadata, the owner ID and a random request identifier. The context never contains the token, hash or pepper (src/Domain/AuthContext.php, src/Http/RequestScope.php). HTTP policy evaluation denies a missing context; the registry additionally requires the current WordPress user to equal the context owner (src/Policy/Policy.php, src/Abilities/Registry.php). STDIO is the only transport where a missing context is allowed.
Live scope intersection. Every term must pass: site enabled, site tier ceiling, admin-tier switch, key tier, enabled toolset, allow/deny lists, third-party approval and the owner's current visibility capability (src/Policy/Policy.php). The scope cannot raise the owner's authority, and a scope update can only narrow the previous one (src/Domain/Scope.php). Tier extensions can only raise the effective risk, and additional policy filters can only deny; neither can grant authority (src/Policy/TierResolver.php). Role changes therefore apply on the next request without reissuing the key.
Per-request owner recheck. Authentication verifies that the key exists, is not revoked, is not expired, belongs to an existing user and that the owner still holds use_xnetvn_agent_hub_wp. It also applies key and site IP allowlists and the site enabled switch before issuing a context (src/Auth/Authenticator.php). Deleting the owner revokes that owner's keys.
HMAC storage with a separate pepper. Only an HMAC-SHA256 digest of the secret is stored, keyed by a separate random 32-byte per-site pepper kept in a non-autoloaded option (src/Auth/KeyHasher.php, src/Auth/PepperStore.php, docs/en/adr/0004-key-model.md). Verification uses a timing-safe comparison. Secret and digest are held outside exportable object properties and are redacted from JSON (src/Auth/StoredKey.php, src/Auth/Pepper.php). Losing the pepper invalidates every key on that site; it never reveals one.
Rate limits and bounded work. A weighted sliding-minute window applies 120 requests per key and 10 failed authentications per IP per minute by default (src/RateLimit/SlidingWindowLimiter.php, src/Persistence/WpSiteSettings.php). A 429 response carries Retry-After (src/Auth/Authenticator.php). Password reauthentication for high-risk key operations has its own 5-per-minute budget, and costly bulk or upload operations use at most two in-flight slots per key (src/RateLimit/WpExpensiveBudget.php).
Origin and TLS checks before credentials. The exact route is guarded before authentication. A missing Origin is accepted, which supports non-browser clients; an explicit null, malformed origin, origin with a path, credentials, query or fragment, or an origin outside the exact allowlist is refused. The site's home and site URL origins are allowed implicitly (src/Http/RouteGate.php, src/Http/OriginGuard.php). TLS must be real, or asserted by X-Forwarded-Proto: https from a peer inside a configured trusted-proxy CIDR. Arbitrary forwarded headers are ignored (src/Http/TransportGuard.php).
Safe responses and audit. MCP responses carry Cache-Control: no-store and X-Content-Type-Options: nosniff; a 401 carries a Bearer challenge and a 429 carries retry guidance (src/Auth/Authenticator.php). The audit writer stores identity, outcome, timing, transport, IP, truncated user agent and top-level argument names only. It never stores argument values, tokens, passwords, hashes or cookies (src/Audit/AuditLogger.php, src/Audit/AuditEvent.php, ADR-0010).
Fail-closed adapter boundary. Only Mcp/ServerGateway depends on adapter classes. The server is created only for the official adapter version range >=0.7.0 <0.8.0; a missing or incompatible adapter leaves the plugin loadable but closes MCP and shows an administrative notice (src/Bootstrap/Requirements.php, src/Mcp/ServerGateway.php, ADR-0003). Third-party abilities are denied unless explicitly approved, and abilities with missing or malformed annotations default to destructive (src/Policy/TierResolver.php). The output pipeline revalidates the schema, redacts sensitive field names, bounds bytes and fails safely (src/Abilities/OutputPipeline.php).
| Threat | Primary control |
|---|---|
| Stolen bearer key replayed from another network | Immediate revocation, expiry, IP allowlists, per-key limit, last-used metadata and unusual-network notice, audit trail |
| CSRF or cookie-session abuse against MCP | Exact-route bearer-only authentication; cookies and Application Passwords rejected; Origin guard |
| DNS rebinding or hostile browser origin | Exact normalized Origin allowlist; a missing Origin is accepted; null denied |
| Agent acts beyond its owner's authority, including after a role downgrade | Live capability intersection; no snapshots; owner existence and use capability rechecked every request; object-level core permissions still run |
| Credential database or backup leak | HMAC-SHA256 with a separate per-site pepper; no plaintext token; hashes hidden from exports and logs |
| Password or key brute force | Timing-safe verification; uniform 401 messages; 10 failed authentications per IP per minute |
| Logs become a secondary secret store | Top-level argument names only; sensitive-name filter; no headers or bodies; optional IP masking; bounded retention |
| Compromised or incompatible adapter | Version gate; single facade; MCP closed when the adapter is absent or outside range |
| Malicious third-party ability | Private registry; explicit name approval; unknown annotations default destructive; output pipeline |
| SSRF through remote media import | Off by default; public HTTPS only; DNS/address and redirect policy; byte, pixel, MIME and time limits |
| Transport downgrade or proxy spoofing | HTTPS required; proxy header accepted only from a trusted CIDR; local exemption limited to local/development |
AGENTS.md lists the repository's security invariants: private abilities only; exact MCP route matching; bearer only on MCP and cookie plus nonce for management; an AuthContext required for HTTP; scope narrows current capabilities; per-request owner recheck; HMAC with a separate pepper; no credentials, hashes or argument values in logs; only Mcp/ServerGateway may depend on adapter classes; every ability has tier, toolset, visibility capability, annotations and a strict schema; and all output passes the output pipeline.
AGENTS.md also requires every relevant original-plan law L-01 through L-20 to be verified before acceptance. The tracked documentation does not reproduce the full list. The one law with an explicit local definition is L-01: registration arguments never set public or show_in_rest, and effective exposure remains false (docs/en/architecture/adapter-notes.md). Treat this page as the control narrative, not as a substitute for the L-01–L-20 acceptance evidence.
See Key-Management for credential lifecycle, Audit-and-Logging for evidence handling, and Troubleshooting-and-FAQ for failure diagnosis.
xNetVN Agent Hub WP · GPL-2.0-or-later · Copyright (c) 2026 xNetVN Inc.
This wiki summarizes the repository documentation. The source of truth is the repository; when they disagree, the repository wins.
Start here
Using the plugin
Security
Engineering
Project