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…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

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.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.