Skip to content

Commit 7d1791d

Browse files
committed
Security 0.9.27: fix GHSA-63gr-g7jc-v8rg — HTTP MCP missing auth
The optional --http / MCP_HTTP=1 transport listened on all interfaces with no Authorization check, so any LAN client could initialize a session and invoke master-key-only tools (setup_email_relay, delete_agent, cleanup_agents, etc.) using the server's own master key. Fix: - Default-bind /mcp to 127.0.0.1 (override with --host= or MCP_HTTP_HOST) - Require Authorization: Bearer <token> on every /mcp request - Token auto-minted to ~/.agenticmail/mcp-http-token (chmod 600), or supplied via --token= / MCP_HTTP_TOKEN - --insecure opt-out for sandboxed test environments only, with a loud warning at startup - /health stays open (returns only session count) - Stdio mode (the default) was never affected Release: mcp 0.9.27, claudecode 0.2.32, codex 0.1.26, cli 0.9.101
1 parent 51aa43e commit 7d1791d

6 files changed

Lines changed: 148 additions & 13 deletions

File tree

agenticmail/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agenticmail/cli",
3-
"version": "0.9.100",
3+
"version": "0.9.101",
44
"description": "Email, SMS & phone-call infrastructure for AI agents — real email addresses, phone numbers, and agent-driven outbound voice calls",
55
"type": "module",
66
"main": "dist/index.js",
@@ -38,8 +38,8 @@
3838
"json5": "^2.2.3"
3939
},
4040
"optionalDependencies": {
41-
"@agenticmail/claudecode": "^0.2.31",
42-
"@agenticmail/codex": "^0.1.25"
41+
"@agenticmail/claudecode": "^0.2.32",
42+
"@agenticmail/codex": "^0.1.26"
4343
},
4444
"devDependencies": {
4545
"tsup": "^8.4.0",

packages/claudecode/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agenticmail/claudecode",
3-
"version": "0.2.31",
3+
"version": "0.2.32",
44
"description": "Claude Code integration for AgenticMail — surfaces every AgenticMail agent as a native Claude Code subagent so any Claude Code session can delegate to them with the Agent tool",
55
"type": "module",
66
"main": "dist/index.js",
@@ -48,7 +48,7 @@
4848
},
4949
"dependencies": {
5050
"@agenticmail/core": "^0.9.37",
51-
"@agenticmail/mcp": "^0.9.26",
51+
"@agenticmail/mcp": "^0.9.27",
5252
"@anthropic-ai/claude-agent-sdk": "^0.2.140"
5353
},
5454
"peerDependencies": {

packages/codex/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agenticmail/codex",
3-
"version": "0.1.25",
3+
"version": "0.1.26",
44
"description": "OpenAI Codex CLI integration for AgenticMail — surfaces every AgenticMail agent as a native Codex subagent and wires the dispatcher daemon to the Codex SDK",
55
"type": "module",
66
"main": "dist/index.js",
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@agenticmail/core": "^0.9.37",
49-
"@agenticmail/mcp": "^0.9.26",
49+
"@agenticmail/mcp": "^0.9.27",
5050
"@iarna/toml": "^2.2.5"
5151
},
5252
"peerDependencies": {

packages/mcp/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ The MCP (Model Context Protocol) server for [AgenticMail](https://github.com/age
88

99
When connected, your AI agent can send emails and texts, check inboxes, reply to messages, receive verification codes, manage contacts, schedule emails, assign tasks to other agents, and more — all through natural language. The server provides 100 tools that cover every email, SMS, and agent management operation.
1010

11+
## ✨ Security — 0.9.27
12+
13+
**Fixes [GHSA-63gr-g7jc-v8rg](https://github.com/agenticmail/agenticmail/security/advisories/GHSA-63gr-g7jc-v8rg)** — missing authentication on the optional Streamable HTTP transport (`--http` / `MCP_HTTP=1`).
14+
15+
- `--http` mode now **binds to `127.0.0.1` by default** and **requires `Authorization: Bearer <token>`** on every `/mcp` request.
16+
- The bearer token is auto-minted on first start and persisted to `~/.agenticmail/mcp-http-token` (chmod 600). Override with `MCP_HTTP_TOKEN` env or `--token=<value>`.
17+
- Bind to other interfaces with `--host=0.0.0.0` or `MCP_HTTP_HOST=...` — startup logs an explicit warning when the endpoint is reachable from the network.
18+
- `--insecure` brings back the old no-auth behavior for sandboxed test environments only. Startup prints a loud warning.
19+
- Stdio mode (the default) was never affected.
20+
21+
If you weren't using `--http` / `MCP_HTTP=1`, no action is needed.
22+
1123
## ✨ What's new in 0.9.0
1224

1325
- **🧠 `get_thread_id` + `save_thread_memory`** — two new tools in the `multi_agent_extras` tier. Workers call `get_thread_id({uid})` once after reading a new message, then `save_thread_memory({threadId, summary, commitments?, openQuestions?, lastAction?, lastUid?})` at end-of-wake. The dispatcher reads the memory back into the next wake's prompt automatically. Pairs with the dispatcher-side ThreadCache to flatten wake cost — agents no longer have to re-read 10 prior messages every time.
@@ -114,6 +126,20 @@ For desktop AI applications, add to your MCP configuration file. Example paths:
114126

115127
¹ Either `AGENTICMAIL_API_KEY` OR `AGENTICMAIL_MASTER_KEY` (or `AGENTICMAIL_ACCOUNT_KEYS_JSON`) must be set, but you don't strictly need all three.
116128

129+
### Optional Streamable HTTP transport (`--http`)
130+
131+
Most users should stick with the default stdio transport — that's what every MCP client config above uses. For environments that need a long-lived HTTP endpoint (browser-based clients, remote-development tunnels, multi-host setups), pass `--http`:
132+
133+
```bash
134+
agenticmail-mcp --http # 127.0.0.1:8014, auth required
135+
agenticmail-mcp --http --port=9001
136+
agenticmail-mcp --http --host=0.0.0.0 # expose on network (token still required)
137+
agenticmail-mcp --http --token=mcphttp_xxx # use a known token instead of the minted one
138+
agenticmail-mcp --http --insecure # sandbox/test only — disables auth
139+
```
140+
141+
The token is read from (in order): `--token=...` flag, `MCP_HTTP_TOKEN` env, `~/.agenticmail/mcp-http-token` (auto-minted on first run). Clients must send `Authorization: Bearer <token>` on every request to `/mcp`. `GET /health` stays open and returns only the session count.
142+
117143
### Per-call identity switching (`_account`)
118144

119145
Every tool's input schema accepts an optional `_account: "<name>"` parameter. When passed, the server resolves that name to an apiKey (from `AGENTICMAIL_ACCOUNT_KEYS_JSON`, then falling back to a live master-keyed lookup of `/accounts`) and runs the call as that agent. Without `_account`, the call uses `AGENTICMAIL_API_KEY` as the default identity.

packages/mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agenticmail/mcp",
3-
"version": "0.9.26",
3+
"version": "0.9.27",
44
"mcpName": "io.github.agenticmail/mcp",
55
"description": "MCP server for AgenticMail — give any AI client real email, SMS, and phone call-control capabilities",
66
"type": "module",

packages/mcp/src/index.ts

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { toolDefinitions, handleToolCall } from './tools.js';
66
import { resourceDefinitions, handleResourceRead } from './resources.js';
77
import { setTelemetryVersion } from '@agenticmail/core';
88
import { createServer } from 'node:http';
9-
import { randomUUID } from 'node:crypto';
9+
import { randomUUID, timingSafeEqual } from 'node:crypto';
10+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
11+
import { homedir } from 'node:os';
12+
import { join as joinPath } from 'node:path';
1013
import { z, type ZodTypeAny } from 'zod';
1114
import { coerceToArray, coerceToObject, coerceToNumber, coerceToBoolean } from './coerce.js';
1215

@@ -299,12 +302,85 @@ function createMcpServer(): McpServer {
299302
const args = process.argv.slice(2);
300303
const httpFlag = args.includes('--http');
301304
const portArg = args.find(a => a.startsWith('--port='));
305+
const hostArg = args.find(a => a.startsWith('--host='));
306+
const tokenArg = args.find(a => a.startsWith('--token='));
307+
const insecureFlag = args.includes('--insecure');
302308
const httpPort = portArg ? parseInt(portArg.split('=')[1], 10) : (parseInt(process.env.MCP_PORT || '', 10) || 8014);
309+
// Default-bind to loopback. Override with --host=0.0.0.0 or MCP_HTTP_HOST
310+
// to expose on other interfaces. Historical behavior (pre-fix for
311+
// GHSA-63gr-g7jc-v8rg) was to bind all interfaces, which exposed the
312+
// admin-tool surface to the LAN.
313+
const httpHost = hostArg ? hostArg.split('=')[1] : (process.env.MCP_HTTP_HOST || '127.0.0.1');
314+
315+
/**
316+
* Resolve the bearer token required to call /mcp in HTTP mode.
317+
*
318+
* Resolution order:
319+
* 1. --token=<value> CLI flag
320+
* 2. MCP_HTTP_TOKEN env var
321+
* 3. Persistent file at ~/.agenticmail/mcp-http-token (auto-minted on
322+
* first run, chmod 600). Survives restarts so a user can wire the
323+
* token into their MCP client config once and forget it.
324+
*
325+
* Returns null only when --insecure is passed. That flag is the explicit
326+
* opt-out and prints a loud warning at startup so it can't happen by
327+
* accident.
328+
*/
329+
function resolveHttpToken(): string | null {
330+
if (insecureFlag) return null;
331+
if (tokenArg) return tokenArg.split('=').slice(1).join('=');
332+
if (process.env.MCP_HTTP_TOKEN) return process.env.MCP_HTTP_TOKEN;
333+
const dir = joinPath(homedir(), '.agenticmail');
334+
const file = joinPath(dir, 'mcp-http-token');
335+
if (existsSync(file)) {
336+
try {
337+
const t = readFileSync(file, 'utf8').trim();
338+
if (t) return t;
339+
} catch { /* fall through to mint */ }
340+
}
341+
const minted = 'mcphttp_' + randomUUID().replace(/-/g, '');
342+
try {
343+
mkdirSync(dir, { recursive: true });
344+
writeFileSync(file, minted + '\n', { mode: 0o600 });
345+
chmodSync(file, 0o600);
346+
} catch (err) {
347+
console.error('[agenticmail-mcp] WARN: could not persist auth token to', file, '—', (err as Error).message);
348+
}
349+
return minted;
350+
}
351+
352+
/**
353+
* Constant-time bearer-token check. Returns true iff the request carries
354+
* `Authorization: Bearer <expected>`. Length-safe so an attacker can't
355+
* distinguish "wrong token" from "wrong length" via timing.
356+
*/
357+
function checkAuth(req: import('node:http').IncomingMessage, expected: string): boolean {
358+
const header = req.headers['authorization'];
359+
if (typeof header !== 'string') return false;
360+
const m = header.match(/^Bearer\s+(.+)$/i);
361+
if (!m) return false;
362+
const got = Buffer.from(m[1]);
363+
const want = Buffer.from(expected);
364+
if (got.length !== want.length) return false;
365+
return timingSafeEqual(got, want);
366+
}
303367

304368
if (httpFlag || process.env.MCP_HTTP === '1') {
305369
// ─── HTTP/Streamable HTTP Transport ───────────────────────────────
306370
// Supports both SSE streaming and direct JSON responses per MCP spec.
307-
// Usage: agenticmail-mcp --http [--port=8014]
371+
// Usage: agenticmail-mcp --http [--port=8014] [--host=127.0.0.1]
372+
// [--token=<bearer>] [--insecure]
373+
//
374+
// Security model (post-GHSA-63gr-g7jc-v8rg):
375+
// - Binds to 127.0.0.1 by default so the admin-tool surface is not
376+
// reachable from other hosts on the network.
377+
// - Requires `Authorization: Bearer <token>` on every /mcp request.
378+
// Token is auto-minted on first run and stored at
379+
// ~/.agenticmail/mcp-http-token (chmod 600). Override with
380+
// MCP_HTTP_TOKEN or --token=.
381+
// - --insecure disables both bind-restriction warnings and the auth
382+
// check. Reserved for sandboxed test environments only.
383+
const authToken = resolveHttpToken();
308384
const server = createMcpServer();
309385

310386
// Map of session ID -> transport for stateful connections
@@ -328,6 +404,21 @@ if (httpFlag || process.env.MCP_HTTP === '1') {
328404
return;
329405
}
330406

407+
// Authentication gate — every /mcp request (POST/GET/DELETE) must
408+
// present the bearer token. Skipped only when --insecure was passed
409+
// (authToken === null), which is logged loudly at startup.
410+
if (authToken !== null && !checkAuth(req, authToken)) {
411+
res.writeHead(401, {
412+
'Content-Type': 'application/json',
413+
'WWW-Authenticate': 'Bearer realm="agenticmail-mcp"',
414+
});
415+
res.end(JSON.stringify({
416+
error: 'Unauthorized. Send Authorization: Bearer <token>. ' +
417+
'Token is at ~/.agenticmail/mcp-http-token or in MCP_HTTP_TOKEN.',
418+
}));
419+
return;
420+
}
421+
331422
// Handle DELETE for session termination
332423
if (req.method === 'DELETE') {
333424
const sessionId = req.headers['mcp-session-id'] as string | undefined;
@@ -392,11 +483,29 @@ if (httpFlag || process.env.MCP_HTTP === '1') {
392483
res.end(JSON.stringify({ error: 'Method not allowed. Use POST /mcp for JSON-RPC, GET /mcp for SSE stream.' }));
393484
});
394485

395-
httpServer.listen(httpPort, () => {
486+
httpServer.listen(httpPort, httpHost, () => {
487+
const displayHost = httpHost === '0.0.0.0' || httpHost === '::' ? 'localhost' : httpHost;
396488
console.log(`🎀 AgenticMail MCP Server (Streamable HTTP)`);
397-
console.log(` Endpoint: http://localhost:${httpPort}/mcp`);
398-
console.log(` Health: http://localhost:${httpPort}/health`);
489+
console.log(` Endpoint: http://${displayHost}:${httpPort}/mcp`);
490+
console.log(` Health: http://${displayHost}:${httpPort}/health`);
491+
console.log(` Bind: ${httpHost}`);
399492
console.log(` Transport: Streamable HTTP (SSE + JSON responses)`);
493+
if (authToken === null) {
494+
console.log('');
495+
console.log(' ⚠️ --insecure: bearer-token auth DISABLED on /mcp.');
496+
console.log(' ⚠️ Anyone who can reach the port can call master-key tools.');
497+
console.log(' ⚠️ Do not run this mode on untrusted networks.');
498+
} else {
499+
console.log(` Auth: Bearer token required on /mcp`);
500+
console.log('');
501+
console.log(' Connect an MCP client with:');
502+
console.log(` Authorization: Bearer ${authToken}`);
503+
if (httpHost !== '127.0.0.1' && httpHost !== 'localhost' && httpHost !== '::1') {
504+
console.log('');
505+
console.log(` ⚠️ Bound to ${httpHost} — endpoint is reachable from the network.`);
506+
console.log(' ⚠️ Make sure the bearer token above is treated as a secret.');
507+
}
508+
}
400509
});
401510

402511
// Graceful shutdown

0 commit comments

Comments
 (0)