Feature 032 — OAuth per-server scoping (SECURITY FIX + BREAKING CHANGE for legacy DCR sessions).
⚠️ BEFORE UPGRADE — READ THIS: this release deletes any pre-F032 DCR-registered OAuth client rows (those without a server-{id}- prefix — e.g., legacy Claude.ai / ChatGPT / Cursor / Cline connections) and their associated tokens + auth codes as part of the D28 upgrade migration. Any live AI-host session bound to a legacy DCR row will disconnect on the next request; affected users must re-run the OAuth authorize flow from their AI host to reconnect. All post-F032 DCR registrations are per-server and unaffected. Consider (a) snapshotting wp_acrossai_mcp_oauth_{clients,tokens,auth_codes} before upgrade, and (b) notifying users with active AI-host connections that they will need to re-authorize once after upgrade.
Security fix: closes a cross-server privilege-escalation gap where an admin on Server A’s Connectors tab could revoke or delete Server B’s clients + tokens by modifying the client_id in the outbound REST body. Also closes a read-side display leak in the “authorized users” listing on the AI Connectors tab.
What ships: adds server_id BIGINT UNSIGNED NOT NULL column (final state) to wp_acrossai_mcp_oauth_clients, wp_acrossai_mcp_oauth_tokens, and wp_acrossai_mcp_oauth_auth_codes via the D28 3-part BerlinDB $upgrades contract (each Table bumps $version 1.0.0 → 1.0.1 with matching upgrade_to_1_0_1() callback). Replaces standalone UNIQUE(client_id) on oauth_clients with composite UNIQUE(client_id, server_id) so the same DCR connector can be registered on multiple MCP servers as independent rows. Every mutating REST endpoint (revoke-client-tokens, delete-client, revoke-connector-tokens) now requires + validates server_id in the body — mismatch returns 403 acrossai_mcp_oauth_cross_server AND fires do_action( ‘acrossai_mcp_oauth_cross_server_attempted’, $client_id, $server_id_requested, $user_id, $timestamp ) (4-arg signature — intentionally does NOT disclose the actual owning server_id to listeners, per SEC-032-001 remediation). DCR endpoint now requires resolvable RFC 8707 resource parameter with mandatory origin verification against home_url() (rejects attacker-origin URLs with 400 invalid_target + fires acrossai_mcp_oauth_dcr_resource_url_origin_mismatch). DCR endpoint also gates against a rare deploy→migration race window with 503 service_unavailable when server_id column is absent (prevents silent destruction of legitimate registrations by the auto-purge step). Backfill of admin clients from server-{id}- prefix includes an orphan-server guard (parsed server_id must exist in wp_acrossai_mcp_servers; otherwise row left NULL and purged alongside legacy DCR rows). Fires do_action( ‘acrossai_mcp_oauth_legacy_dcr_purged’, $clients_deleted, $tokens_deleted, $auth_codes_deleted ) exactly once per upgrade run for operator observability. UserLifecycle::on_user_deleted() cascade preserved unchanged (site-wide per FR-042 — regression-tested).
F032 extended scope (folded in-branch): new BerlinDB module wp_acrossai_mcp_connector_approved_users promotes admin-approval state from serialized wp_options to a first-class relational table (FR-029); new “Approved Users” admin panel (FR-046..FR-048) surfaces between Connections and Settings when require_admin_approval is enabled; new revoke-approval → token-revoke cascade wired via acrossai_mcp_connector_user_approval_revoked action with opt-out filter acrossai_mcp_connector_revoke_tokens_on_approval_revoked (FR-040/FR-041); new “Revoke from all servers” nuclear button (FR-043 — deliberate D31 carve-out, fires acrossai_mcp_oauth_client_revoked_across_all_servers NEVER acrossai_mcp_oauth_cross_server_attempted); Access Control connection-time gate now enforces at OAuth authorize + CLI device-grant + Application Password generation (FR-049) so denied users see immediate access_denied instead of confusing “connected then silent 403” behavior; annotated token counts (2 (1 access · 1 refresh)) replace opaque totals in Connections panel (FR-045); enriched AC 403 with server_slug + user_roles (FR-050).
Admins bypass require_admin_approval (FR-051): users with manage_options capability skip the pending-approval queue entirely and are auto-added to the Approved Users list on first connection with approved_by = $user_id (self-approval). Rationale: admins can approve themselves in one click from the Approved Users panel anyway; the pending detour is UX friction, not a security boundary. Effective threat model unchanged (admins already have manage_options, a strict superset of any approval decision they could make on themselves). The Settings-panel description under “Require admin approval for new connections” includes an inline note explaining this so operators are not confused when they connect as admin and skip the queue.
Feature 022 — Shared AcrossAI Add-ons submenu. The plugin now registers the shared “Add-ons” nav entry under the AcrossAI top-level menu, powered by Freemius for product id 34418. The page requires install_plugins; when a companion AcrossAI plugin is active simultaneously only one plugin contributes the nav entry (the shared package coordinates this so operators never see duplicate submenu rows). Bumps acrossai-co/main-menu from 0.0.14 to 0.0.18. 0.0.15 enabled the Freemius Account, Contact Us, and wp.org Support Forum submenus at package level; 0.0.16 promotes those defaults to FreemiusInitializer::DEFAULT_MENU and introduces a new fs_menu key on AddonsPage‘s $args array so each consumer plugin explicitly decides which auto-submenus surface. 0.0.17 disables the vendor’s own MenuRegistrar::register() — Freemius’s menu.addons submenu (enabled here via fs_menu.addons = true) is now the sole source of the Add-ons row, per the AcrossAI “umbrella product” model where Freemius product 34418 (acrossai-add-ons) owns the single ecosystem-wide Add-ons page. 0.0.18 adds an fs_has_addons key on AddonsPage‘s $args — Freemius’ SDK gates the Add-ons row on if ( $this->has_addons() ) (class-freemius.php:18964), so menu.addons => true alone was insufficient. The plugin now passes fs_has_addons => true explicitly, which forwards to fs_dynamic_init() and unblocks the Add-ons row. The plugin passes an explicit fs_menu array in includes/Main.php declaring every key so the full menu policy is visible at the call site — flip any boolean there to change what operators see without a vendor release. Adds an explicit VCS repositories entry for acrossai-co/main-menu in composer.json so consumers resolve deterministically from GitHub without waiting on Packagist sync.
Operator action required for pre-016 installs. The plugin ships fresh-install-only — no in-plugin schema migration. Before reactivating the updated plugin on an install that had populated Claude Connector data, run this manual retirement recipe (SEC-016-001 defense-in-depth: the pre-DROP UPDATE forces the InnoDB tablespace to overwrite the plaintext client_secret bytes before the column is dropped):
UPDATE wp_acrossai_mcp_servers SET
claude_connector_client_secret = ”,
claude_connector_redirect_uri = ”;
DROP TABLE IF EXISTS wp_acrossai_mcp_oauth_tokens;
DROP TABLE IF EXISTS wp_acrossai_mcp_oauth_audit;
ALTER TABLE wp_acrossai_mcp_servers
DROP COLUMN claude_connector_client_id,
DROP COLUMN claude_connector_client_secret,
DROP COLUMN claude_connector_redirect_uri;
DELETE FROM wp_options WHERE option_name IN (
‘acrossai_mcp_oauth_tokens_db_version’,
‘acrossai_mcp_oauth_audit_db_version’,
‘acrossai_mcp_claude_connectors_enabled’
);
And one companion WP-CLI step to clear the retired daily cron: wp cron event unschedule acrossai_mcp_oauth_cleanup. If your install has any active claude.ai Connector tokens, revoke them from claude.ai’s Connectors UI BEFORE running the retirement SQL — the retirement drops the audit log with no recovery.
Plugin Website
Visit website
Share Post
Get insights into what’s happening at ChangelogWP right in your inbox. We don’t believe in spam.