Back to Security Basics
Brooks McMillin
"Built and broke my own MCP server so you don't have to."
Slides
Model Context Protocol — an open standard from Anthropic for connecting AI agents to external tools and data.
97M
monthly SDK downloads
16,000+
MCP servers deployed
8.5%
use OAuth
437K+
devs hit by CVE-2025-6514
Authentication is OPTIONAL in the spec. 70–80% of servers have none at all. The rest rely on static secrets.
Production breaches in 2025: Smithery Registry (3,000+ apps exposed), Asana cross-tenant access, GitHub prompt injection leaking private repos, Postmark supply-chain BCC attack.
The mcp-remote npm package passed OAuth metadata directly
to the system shell:
Impact: Full code execution on Claude Desktop, Cursor, Windsurf, VS Code. 437,000+ installs before patch.
Lesson: OAuth metadata is untrusted input. Validate URLs. Never pass to shell.
I built a TaskManager MCP Server. OAuth 2.0 auth server, device flow,
PKCE, token introspection, the works.
Then I audited it.
Prompt injection · Data exfiltration
The client is no longer a dumb HTTP library. It's a reasoning engine that can be lied to.
Prompt injection can be embedded anywhere the agent ingests text:
Task titles, descriptions, comments, filenames, user profiles — any content fetched via MCP tools.
Malicious MCP servers ship poisoned tool descriptions. Agent reads them to decide what tools do.
API responses, error messages, metadata fields — anything returned from a tool call.
Web pages, documents, emails — content the agent fetches or summarizes on your behalf.
The agent has OAuth tokens + multiple tools. Every injection surface is a potential privilege escalation.
Two MCP servers connected to the same AI agent:
The prompt:
"Get my todos, then check the weather"
Three injection vectors in one tool description. No jailbreaking required — the LLM treats tool schemas as trusted instructions.
Pin known tool schemas. Reject tools whose descriptions change between sessions. Signed tool manifests.
Partially exists — no MCP standardRequire user confirmation before sending emails, writing data, or calling tools outside the original request scope.
Claude Code's permission model does thisTaint-track tool outputs. Restrict which tools can consume data from other trust domains.
Does not exist in MCP todayFlag suspicious patterns: freeform "context" params, minimum lengths on text fields, instructions in descriptions.
Does not exist in MCP todayToday's real defense: don't install untrusted MCP servers. That's "don't click suspicious links" for the agentic era.
Key pattern: Screen at the MCP tool layer, before any backend action.
This already happened. CVSS 9.3.
ServiceNow's Now Assist AI Agents — their enterprise chatbot and agentic AI platform — had a critical auth bypass discovered by AppOmni researcher Aaron Costello:
"ServiceNow shipped the same credential to every third-party service that authenticated to the Virtual Agent"
"As far as ServiceNow was concerned, all a user needed to prove their identity was their email address"
"ServiceNow decided to upgrade its virtual agent...[allowing] users to create new data anywhere in ServiceNow"
"Costello used it to create a new account for himself in the system, with admin-level privileges"
The AI agent became the weapon.
Source: Dark Reading
PKCE · DNS Rebinding
PKCE (Proof Key for Code Exchange) is like a claim ticket: you generate a secret, send a hash of it with your auth request, then prove you have the original secret when exchanging the code. If someone steals the code, they can't use it without the secret.
code_verifier needed
In MCP, almost every client is a CLI, desktop app, or extension — public clients where PKCE is essential.
Fix: Store
code_challenge at the MCP auth layer, validate
code_verifier on token exchange.
Key insight: MCP servers are OAuth
proxies — PKCE must be validated here, not at the backend.
CVE-2025-66414/66416 (CVSS 7.6) — Malicious websites can bypass same-origin policy to attack local MCP servers:
127.0.0.1:3000
evil.com —
DNS initially resolves to attacker's IP
evil.com rebinds to 127.0.0.1
Root cause:
enableDnsRebindingProtection was
disabled by default in both TypeScript and Python
SDKs.
NeighborJack (June 2025): Found
492 MCP servers binding to 0.0.0.0 with
no authentication — complete host system takeover.
Storage · Timing · Defaults
.env.example shipped with SECRET_KEY=change-me-in-production.env and deploySECRET_KEY@model_validator blocks startup if default secret detected in production8 times.
I found this pattern eight separate times across the codebase:
Methodology · Checklists
When reviewing an MCP implementation, work the chain:
.well-known/*?
* on OAuth endpoints
Slides
Sample Code
Resources: modelcontextprotocol.io · RFC 7636 (PKCE) · RFC 8628 (Device Flow) · RFC 8707 (Resource Indicators) · lakera.ai