GHSA-FQ4X-789W-JG5H

Vulnerability from github – Published: 2026-06-18 17:21 – Updated: 2026-07-21 13:41
VLAI
Summary
AgenticMail: Unauthenticated inbound mail triggers bypassPermissions resume of the operator's Claude Code session (bridge-wake)
Details

Summary

Two inbound-mail handlers act on a privileged effect without verifying that the sender is the operator, while a sibling handler in the same repo does. The higher-impact one: any external email routed to the bridge inbox causes the dispatcher to resume the operator's Claude Code session with permissionMode: 'bypassPermissions', embedding the attacker-controlled from/subject/preview verbatim into the prompt the resumed agent reads — an indirect prompt injection into a fully-privileged agent (Bash/Write/Edit/WebFetch + the agenticmail MCP toolbelt) running as the operator's OAuth identity. The sibling operator-query email-reply hook gates the same untrusted-From provenance with isOperatorReplySender(replyFrom, config.operatorEmail) (fail-closed); the bridge-wake path — a strictly higher-privilege effect — has no equivalent.

Affected code (current HEAD, commit b95f52e)

Untrusted provenance: external inbound email enters at packages/api/src/routes/inbound.ts:41 (POST /mail/inbound); the x-inbound-secret authenticates only the relay->API hop, not the external sender, so from/subject/preview are attacker-controlled.

Privileged sink (bridge-wake, bypassPermissions): - packages/claudecode/src/dispatcher.ts:2040 handleBridgeMail extracts subject/from/preview (:2045-2049) and calls planBridgeWake({ session, mail: { ..., from, preview } }) (:2052) with NO sender check — routing keys only on session freshness (skip-live / escalate / resume). - planBridgeWake -> packages/core/src/host-bridge.ts:141 composeBridgeWakePrompt embeds the untrusted from/subject/preview (preview sliced to 600 chars at :144) verbatim into the prompt. - packages/claudecode/src/bridge-wake.ts:103 resumeBridgeSession runs the prompt via the Claude Code SDK with permissionMode: 'bypassPermissions' against the operator's last session (resume + same mcpServers).

Guarded sibling (same class, authenticated): packages/api/src/routes/inbound.ts:102 rejects an operator-query email reply unless isOperatorReplySender(replyFrom, config.operatorEmail) (def packages/core/src/phone/realtime-tools.ts:999, fail-closed when no operatorEmail), with a v0.9.53 security-review comment (inbound.ts:93-100) stating inbound mail provenance is untrusted and an emailed answer is only honored when its From matches the configured operator. The Telegram sibling likewise gates on operatorChatId. The bridge-wake path ignores this exact lesson.

Secondary instance (same root cause): packages/core/src/gateway/manager.ts:261 tryProcessApprovalReply releases a held outbound email on an "approve" reply matched only by In-Reply-To / notification_message_id, with no sender check — again unlike the isOperatorReplySender sibling.

Impact

In a configured headless-bridge deployment (operator uses the CLI so a host-session is saved; session fresh <24h per host-sessions.ts:127; external mail routed to the bridge inbox via relay sub-addressing or a Cloudflare webhook), an external sender achieves indirect prompt injection into a bypassPermissions operator session -> arbitrary OS command execution, filesystem read/write, and exfiltration under the operator's OAuth identity. The auth gap (no sender check on the bridge path) is structural and unconditional; the impact realization is config-conditional and depends on the resumed model following injected instructions.

Proof of concept (static / request-difference; dynamic on operator's OWN setup only)

Static: the from/subject/preview extracted at dispatcher.ts:2045-2049 flow into composeBridgeWakePrompt (host-bridge.ts:141) and resumeBridgeSession (bridge-wake.ts:103) with no interposed sender check, while the sibling inbound.ts:102 has one — the same untrusted-From provenance is authenticated on one privileged email path and not on the higher-privileged one. Dynamic (own instance only): with a configured bridge + fresh host-session, send mail from a non-operator address into the bridge inbox whose subject/preview contains a benign instruction writing a fresh CSPRNG marker; observe the resumed bypassPermissions session act on it. Use only your own instance; do not target third-party deployments.

Suggested fix

Mirror the guarded sibling: before any bypassPermissions resume (dispatcher.ts handleBridgeMail, before planBridgeWake), require trusted provenance — an internal sub-agent wake OR isOperatorReplySender(from, config.operatorEmail); otherwise deliver the mail normally but do NOT resume. Reuse the existing exported isOperatorReplySender from @agenticmail/core so the two privileged email paths share one authentication helper. Defense-in-depth: in composeBridgeWakePrompt, wrap the untrusted fields in explicit untrusted-data delimiters and drop bypassPermissions for mail-triggered resumes whose provenance is not the operator. Apply the same sender gate to tryProcessApprovalReply (manager.ts:261).

Affected versions

Present on current HEAD (core 0.9.42 / claudecode 0.2.38, commit b95f52e). No fix retrofits the sender check onto bridge-wake.

Severity (honest, both ways)

HIGH, plausibly CRITICAL in a configured headless-bridge deployment. Ceiling ~9.0 (unauthenticated external sender -> operator-privileged code execution). Floor ~7.0: the auth gap is unconditional, but full impact requires (1) a fresh <24h host-session, (2) external mail routed to the bridge inbox, (3) the resumed model obeying injected instructions (non-deterministic). Not a deterministic RCE primitive. CWE-306 (missing authentication for the privileged action) + CWE-77/CWE-94 (injected instructions realized as command execution). Novelty: the two existing agenticmail advisories (CVE-2026-50287 MCP missing-auth; CVE-2026-47255 storage SQL) do not cover this sink. Please rate per your deployment assumptions.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@agenticmail/core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.9.43"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@agenticmail/claudecode"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.2.39"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@agenticmail/codex"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.1.33"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@agenticmail/openclaw"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.5.71"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-57495"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-18T17:21:27Z",
    "nvd_published_at": "2026-07-20T22:17:17Z",
    "severity": "HIGH"
  },
  "details": "## Summary\nTwo inbound-mail handlers act on a privileged effect without verifying that the sender is the operator, while a sibling handler in the same repo does. The higher-impact one: any external email routed to the bridge inbox causes the dispatcher to resume the operator\u0027s Claude Code session with `permissionMode: \u0027bypassPermissions\u0027`, embedding the attacker-controlled `from`/`subject`/`preview` verbatim into the prompt the resumed agent reads \u2014 an indirect prompt injection into a fully-privileged agent (Bash/Write/Edit/WebFetch + the agenticmail MCP toolbelt) running as the operator\u0027s OAuth identity. The sibling operator-query email-reply hook gates the same untrusted-From provenance with `isOperatorReplySender(replyFrom, config.operatorEmail)` (fail-closed); the bridge-wake path \u2014 a strictly higher-privilege effect \u2014 has no equivalent.\n\n## Affected code (current HEAD, commit b95f52e)\nUntrusted provenance: external inbound email enters at `packages/api/src/routes/inbound.ts:41` (POST /mail/inbound); the `x-inbound-secret` authenticates only the relay-\u003eAPI hop, not the external sender, so `from`/`subject`/`preview` are attacker-controlled.\n\nPrivileged sink (bridge-wake, bypassPermissions):\n- `packages/claudecode/src/dispatcher.ts:2040` `handleBridgeMail` extracts `subject`/`from`/`preview` (`:2045-2049`) and calls `planBridgeWake({ session, mail: { ..., from, preview } })` (`:2052`) with NO sender check \u2014 routing keys only on session freshness (skip-live / escalate / resume).\n- `planBridgeWake` -\u003e `packages/core/src/host-bridge.ts:141` `composeBridgeWakePrompt` embeds the untrusted `from`/`subject`/`preview` (preview sliced to 600 chars at `:144`) verbatim into the prompt.\n- `packages/claudecode/src/bridge-wake.ts:103` `resumeBridgeSession` runs the prompt via the Claude Code SDK with `permissionMode: \u0027bypassPermissions\u0027` against the operator\u0027s last session (resume + same mcpServers).\n\nGuarded sibling (same class, authenticated): `packages/api/src/routes/inbound.ts:102` rejects an operator-query email reply unless `isOperatorReplySender(replyFrom, config.operatorEmail)` (def `packages/core/src/phone/realtime-tools.ts:999`, fail-closed when no operatorEmail), with a v0.9.53 security-review comment (`inbound.ts:93-100`) stating inbound mail provenance is untrusted and an emailed answer is only honored when its From matches the configured operator. The Telegram sibling likewise gates on `operatorChatId`. The bridge-wake path ignores this exact lesson.\n\nSecondary instance (same root cause): `packages/core/src/gateway/manager.ts:261` `tryProcessApprovalReply` releases a held outbound email on an \"approve\" reply matched only by `In-Reply-To` / `notification_message_id`, with no sender check \u2014 again unlike the `isOperatorReplySender` sibling.\n\n## Impact\nIn a configured headless-bridge deployment (operator uses the CLI so a host-session is saved; session fresh \u003c24h per `host-sessions.ts:127`; external mail routed to the bridge inbox via relay sub-addressing or a Cloudflare webhook), an external sender achieves indirect prompt injection into a `bypassPermissions` operator session -\u003e arbitrary OS command execution, filesystem read/write, and exfiltration under the operator\u0027s OAuth identity. The auth gap (no sender check on the bridge path) is structural and unconditional; the impact realization is config-conditional and depends on the resumed model following injected instructions.\n\n## Proof of concept (static / request-difference; dynamic on operator\u0027s OWN setup only)\nStatic: the from/subject/preview extracted at dispatcher.ts:2045-2049 flow into composeBridgeWakePrompt (host-bridge.ts:141) and resumeBridgeSession (bridge-wake.ts:103) with no interposed sender check, while the sibling inbound.ts:102 has one \u2014 the same untrusted-From provenance is authenticated on one privileged email path and not on the higher-privileged one. Dynamic (own instance only): with a configured bridge + fresh host-session, send mail from a non-operator address into the bridge inbox whose subject/preview contains a benign instruction writing a fresh CSPRNG marker; observe the resumed bypassPermissions session act on it. Use only your own instance; do not target third-party deployments.\n\n## Suggested fix\nMirror the guarded sibling: before any `bypassPermissions` resume (dispatcher.ts handleBridgeMail, before planBridgeWake), require trusted provenance \u2014 an internal sub-agent wake OR `isOperatorReplySender(from, config.operatorEmail)`; otherwise deliver the mail normally but do NOT resume. Reuse the existing exported `isOperatorReplySender` from @agenticmail/core so the two privileged email paths share one authentication helper. Defense-in-depth: in composeBridgeWakePrompt, wrap the untrusted fields in explicit untrusted-data delimiters and drop `bypassPermissions` for mail-triggered resumes whose provenance is not the operator. Apply the same sender gate to `tryProcessApprovalReply` (manager.ts:261).\n\n## Affected versions\nPresent on current HEAD (core 0.9.42 / claudecode 0.2.38, commit b95f52e). No fix retrofits the sender check onto bridge-wake.\n\n## Severity (honest, both ways)\nHIGH, plausibly CRITICAL in a configured headless-bridge deployment. Ceiling ~9.0 (unauthenticated external sender -\u003e operator-privileged code execution). Floor ~7.0: the auth gap is unconditional, but full impact requires (1) a fresh \u003c24h host-session, (2) external mail routed to the bridge inbox, (3) the resumed model obeying injected instructions (non-deterministic). Not a deterministic RCE primitive. CWE-306 (missing authentication for the privileged action) + CWE-77/CWE-94 (injected instructions realized as command execution). Novelty: the two existing agenticmail advisories (CVE-2026-50287 MCP missing-auth; CVE-2026-47255 storage SQL) do not cover this sink. Please rate per your deployment assumptions.",
  "id": "GHSA-fq4x-789w-jg5h",
  "modified": "2026-07-21T13:41:07Z",
  "published": "2026-06-18T17:21:27Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/security/advisories/GHSA-fq4x-789w-jg5h"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-57495"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/agenticmail/agenticmail"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "AgenticMail: Unauthenticated inbound mail triggers bypassPermissions resume of the operator\u0027s Claude Code session (bridge-wake)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…