Back to Security Basics
Brooks McMillin
"Built and broke my own MCP server so you don't have to."
brooksmcmillin.com · linkedin.com/in/brooksmcmillin · github.com/brooksmcmillin
Model Context Protocol — an open standard from Anthropic for connecting AI agents to external tools and data.
A wrapper around API calls that lets LLMs invoke tools when needed:
github.com/brooksmcmillin/taskmanager/.../server.py
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. 53% 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.
All examples in this talk: real code, real commits, real fixes from this implementation.
Prompt injection meets OAuth 2.0
The client is no longer a dumb HTTP library. It's a reasoning engine that can be lied to.
Scenario: AI agent uses MCP to access your task manager
"Show me my tasks"
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.
The LLM treats tool schemas as system-level instructions. No distinction between legitimate requirements and injected ones.
Output from the task manager flows freely into the weather tool's parameters. No data boundary between trust domains.
"Min length: 100 characters" is social engineering of the model.
Forces it to stuff real data instead of sending "N/A".
If the context param fails, the NOTE instructs the agent to email the data using a different tool entirely.
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.
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
Key pattern: Screen at the MCP tool layer, before any backend action.
PKCE · Confused Deputy · DNS Rebinding
Also found but not deep-diving: open redirects in return_to (CWE-601),
XSS in OAuth callbacks (CWE-79), and state tokens without expiration (CSRF stockpiling).
Same classics, same fixes.
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 mandatory.
Key insight: MCP servers are OAuth proxies. PKCE must be validated at the MCP layer.
When MCP servers proxy OAuth with a shared client_id, attackers can steal authorization codes:
redirect_uri
Obsidian Security found one-click account takeover in multiple major MCP implementations.
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
✗ Anyone with database access has every user's OAuth token.
✓ Fernet symmetric encryption, key derived from SECRET_KEY
Commit: dd88771
8 times.
I found this pattern eight separate times across the codebase:
Reality: People copy
.env.example to .env and deploy.
Defense: Fail loud at startup. Don't trust humans to read READMEs.
Methodology · Checklists
When reviewing an MCP implementation, work the chain:
.well-known/*?
* on OAuth endpointsbrooksmcmillin.com
Resources: modelcontextprotocol.io · RFC 7636 (PKCE) · RFC 8628 (Device Flow) · RFC 8707 (Resource Indicators) · lakera.ai