Skip to content

Email abilities are capability-gated, not ownership-gated — blocks per-user mailboxes for MCP #3507

Description

@chubes4

Blocker for MCP launch

Extra-Chill/extrachill-mcp exposes the ability surface to external AI clients (ChatGPT, Claude, Gemini), scoped to the connected user's permissions. The intended product behaviour is that a user connects their own mailbox and asks their own AI client about their own email.

Today the email abilities cannot express that, and the gap is an authorization hole rather than a missing feature.

Current behaviour

FetchEmailAbility::checkPermission():

public function checkPermission(): bool {
    return PermissionHelper::can( 'use_tools' ) || PermissionHelper::can_manage();
}

A flat capability check. No owner check, and the credential has no owner to check against — auth_ref defaults to email_imap:default, a single global handle.

So any user holding use_tools (the team tier on this install: several people) can fetch-email, email-reply, send-email, email-delete, email-move, and email-batch-unsubscribe against whatever mailbox is configured. Not because they were granted that mailbox — because the ability never models mailbox ownership.

send-email / email-reply additionally means sending as that identity.

Not currently exploitable: no mailbox is configured on this install and the PHP IMAP extension is absent, so execution errors early. This is about the shape being wrong before it is populated, not a live incident.

The primitive already exists and is already used elsewhere

public static function owns_resource( int $resource_user_id, string $action = 'manage_flows' ): bool {
    if ( 0 === $resource_user_id ) { return true; }            // single-agent mode
    if ( self::has_privileged_resource_access( $action ) ) { return true; }
    return self::acting_user_id() === $resource_user_id;
}

Real consumers today: ExecutionScope::owns_agent_resource(), Job/JobHelpers.php:73, Api/RestAccessGuard.php:107. Jobs and agent-scoped resources are correctly owner-checked.

The email abilities simply never adopted it. scope_user_id() has zero call sites anywhere in the plugin.

Work

  1. Give email auth refs an owner. email_imap:default becomes a per-user record carrying user_id. This is the substantive change; the rest follows from it. Decide explicitly what happens to an existing unowned/global ref — owns_resource() already treats user_id === 0 as shared, which may be the right migration path for a genuinely shared operational mailbox, but that should be a decision rather than an accident.
  2. Gate the email abilities on ownership, resolving the ref's owner and calling owns_resource() — matching what JobHelpers and RestAccessGuard already do. Small once step 1 lands.
  3. Provide a connect flow so a user can attach their own mailbox.

Wider than email

Any ability acting on a shared credential has this shape: gated on a capability rather than on holding the credential. data-machine-socials publishing is the obvious sibling — though a brand's Instagram is genuinely shared in a way an inbox is not, so the answer there may legitimately differ.

The model that gets this right already exists on the platform: venue grants in extrachill-events resolve booking_id to a venue and check that venue's grant per call. You get the venues you were granted, not all venues.

Worth deciding as a rule rather than case by case: abilities that act on a credential should be authorized by ownership of that credential, not by a generic tool-use capability.

Related

  • Extra-Chill/extrachill-mcp#7 — network-wide MCP endpoint (merged)
  • Extra-Chill/extrachill-network#208 — audit of the writes any account can reach
  • Extra-Chill/extrachill-network#207 — unfilterable WP-CLI permission bypasses

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions