How I found a CVSS 9.8 Critical in an OAuth-protected MCP server. And the one line fix.

calendar_today agoschedule4 min read
— Originally published at dev.to

Background

MCP (Model Context Protocol) is Anthropic's open standard for connecting LLM agents to external tools. A few of these tools include databases, file systems, APIs and shell access. MCP is only 18 months old, and the ecosystem is growing fast. This means very few of those servers have been looked at by anyone with a security background.

I've been auditing community MCP servers as part of research at Shroud Labs. This post covers the most interesting finding so far: a CVSS 9.8 Critical authentication bypass in a server explicitly advertising OAuth 2.1 protection.

The target

A TypeScript MCP server implementing the MCP spec's OAuth 2.1 authorization extension. Its entire purpose is to be an example of how to build a secure remote MCP server with proper bearer token authentication. It ships in two variants: a stateful version (maintains session state) and a stateless version (no session state, suitable for serverless deployments).

The README describes it as production-ready. It's installable and configured against a real Auth0 tenant.

The hunt

I built a specialised TLAD scanner (Transport-Layer Auth Divergence): a Semgrep ruleset I built to detect a specific pattern. This detects if auth middleware is applied to one transport variant but missing on another. I ran the scanner across 49 community HTTP MCP servers.

For this repo, it flagged both an authed mount and an unguarded mount in the same codebase. I manually checked for differences in the two files for the Layer 2 review. Took me 2 minutes.

app.stateful.ts line 150:

typescript
app.post("/mcp", bearerAuthMiddleware, async (req, res, next) => {

app.stateless.ts line 107:

typescript

app.post("/mcp", async (req: Request, res: Response, next: NextFunction) => {

One argument. bearerAuthMiddleware is defined a few lines earlier in both files. It's used correctly in the stateful version. On the stateless version's /mcp route, it's completely missing.

The middleware was created. It was wired in everywhere else. It was just never passed to the one route that matters.

Why this is actually critical

bearerAuthMiddleware is what calls requireBearerAuth() — the function that validates the OAuth token. Without it in the middleware chain, the route handler runs regardless of what the caller sends.

That means:

No Authorization header → 200 OK
Fake bearer token → 200 OK
Revoked token → 200 OK
Someone else's token → 200 OK

The OAuth flow, the Auth0 tenant, the token issuance, the scope checking. It's all rendered irrelevant. The auth check never fires, and every tool exposed by the server is invocable by anyone who can reach the HTTP endpoint.

Dynamic confirmation

To be certain, I stood up the server locally with dummy OAuth credentials and ran two requests:

bash

Request 1: no token at all

curl -s -X POST http://localhost:5050/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

HTTP 200 — full tool list returned

Request 2: fake bearer token

curl -s -X POST http://localhost:5050/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer notarealtoken" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

HTTP 200 — full tool list returned again

The server logs confirmed both requests hit the route handler. Neither was rejected.

For comparison, the stateful version correctly returns 401 Unauthorized on both requests. Same codebase, different file, one missing argument.

CVSS 9.8

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AV:N — the server binds to a network port for HTTP access. That's its whole purpose.
AC:L — no special conditions. Just send a request.
PR:N — no credentials required. Obviously.
UI:N — no user interaction.
C:H / I:H / A:H — complete access to all tools. Depending on what tools are configured, that's data access, writes, or arbitrary actions.

9.8 is the right score. The server is explicitly designed for network deployment.

The fix

One line, in app.stateless.ts:

typescript
// Before
app.post("/mcp", async (req, res, next) => {

// After
app.post("/mcp", bearerAuthMiddleware, async (req, res, next) => {

Identical to what the stateful version already does correctly. The middleware is already defined and imported. It just needs to be passed to this route.

The lesson

The bug lived in a diff between two files that were supposed to be equivalent in their security properties.

Security bugs aren't always exotic. Sometimes the control exists, is tested elsewhere, is working correctly on the other variant — and was simply never wired to the specific code path that runs in production.

When you're auditing a server that has multiple transport modes, multiple deployment variants, or multiple files implementing similar functionality: compare them. The gap between them is where the bug lives.

Rule: when two files should be identical in their security properties, diff them. The gap is the finding.

CVE and tooling

This was disclosed as CVE-2026-2035999 (CVSS 9.8 Critical).

The TLAD scanner that surfaced it is open source: {% embed github.com/ShroudLabs-io/tlad-semgrep %}

A companion general-purpose sink ruleset for MCP servers (mcp-sinks) covers command injection, path traversal, SSRF, SQLi, and deserialisation across Python and TypeScript.
{% embed https://github.com/ShroudLabs-io/mcp-sinks.git %}

I surveyed 49 community HTTP MCP servers with these tools. Of the published packages with HTTP exposure in the sample, 100% had no inbound authentication. The ecosystem is young and largely unaudited. The tooling is now open.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

I Wrote a Script to Fix Audible's Unreadable PDF Filenames

snapsynapseverified - Apr 20

MCP Is the USB-C of AI. So Why Are You Plugging Everything In?

Ken W. Algerverified - Jun 10

Breaking the AI Data Bottleneck: How Hammerspace's AI Data Platform Eliminates Migration Nightmares

Tom Smithverified - Mar 16
chevron_left
121 Points2 Badges
1Posts
0Comments
Founder of Shroud Labs. Security researcher. Published CVEs in the MCP ecosystem: CVE-2026-2035999 (... Show more

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!