Back to Blog
high SEVERITY8 min read

CVE-2026-104850: MCP TypeScript SDK OAuth Credential Leak

The MCP TypeScript SDK (`@modelcontextprotocol/sdk`) contained a flaw in its OAuth client flow where the MCP server being connected to could influence which authorization server the client talked to, meaning client credentials and token-exchange traffic could be directed to an endpoint the attacker controls. This project was pinned to the affected `1.30.0`; the dependency has been moved to `^1.31.0`, which carries the upstream fix for CVE-2026-104850. Any MCP client that performs OAuth against t

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 7, 2026•Reviewed October 7, 2026

Answer Summary

`@modelcontextprotocol/sdk` on npm, as used by MCP clients that call the SDK's `auth()` / `OAuthClientProvider` flow, is affected on versions before `1.31.0` — this repository was pinned to `1.30.0`. A malicious or compromised MCP server could nominate an authorization server of its choosing during discovery, causing the client to send OAuth client credentials and authorization-code exchange requests to an attacker-controlled endpoint, yielding registered client secrets and usable access tokens. The fix is to upgrade to `1.31.0`, which constrains the authorization server the client will trust for a given MCP server; the dependency declaration here changed from `1.30.0` to `^1.31.0`. No CWE has been assigned to CVE-2026-104850 at the time of writing.

Vulnerability at a Glance

cweN/A
fixUpgrade `@modelcontextprotocol/sdk` from `1.30.0` to `^1.31.0`
riskA hostile MCP server can harvest OAuth client credentials and authorization codes/tokens from connecting MCP clients
languageTypeScript / JavaScript (Node.js)
root causeThe authorization server used by the SDK's OAuth flow was derived from data the MCP server controls, without being bound to the MCP server's own origin
vulnerabilityOAuth client sends credentials to an authorization server chosen by the relying MCP server (credential disclosure via untrusted discovery)

Summary

The MCP TypeScript SDK's OAuth client flow trusted the MCP server too far: the server being authenticated to could influence which authorization server the client authenticated at. That inverts the trust relationship an OAuth client depends on, and it means a hostile MCP server can collect credentials that were never meant for it. This repository pinned the affected @modelcontextprotocol/sdk@1.30.0; the dependency now resolves to ^1.31.0, which carries the upstream fix for CVE-2026-104850.

Introduction

In the Model Context Protocol, an MCP client connecting over HTTP that receives a 401 is expected to bootstrap OAuth from the server's own response: read the WWW-Authenticate challenge, fetch protected resource metadata, learn which authorization server guards that resource, then register a client (often dynamically) and run an authorization-code exchange. The SDK packages all of that behind auth() and the OAuthClientProvider interface so that application authors never hand-roll the dance.

The problem is that every input to that discovery chain originates with the MCP server — the very party whose honesty is in question. CVE-2026-104850 describes exactly that failure mode: the OAuth client "could send credentials to an authorization server chosen by the MCP server." If the resource metadata or the advertised authorization_servers entry is not bound back to the MCP server's own origin and an operator-supplied allowlist, then the endpoint receiving your client_id, client_secret, and authorization code is picked by whoever wrote the MCP server manifest.

This matters well beyond MCP. Any client that performs discovery-driven OAuth — OpenID Connect issuer discovery, RFC 9728 protected resource metadata, .well-known lookups keyed off a redirect or a header — has the same shape of bug waiting in it. Discovery tells you where to go; it must never be the thing that decides whom to trust.

Affected Versions

Affected unknown lower bound, through 1.30.0 (the version pinned in this project)
Fixed in 1.31.0
Ecosystem npm
CVE / GHSA CVE-2026-104850 / not assigned
CWE unknown

Upstream did not publish a precise affected range alongside the identifier, so treat anything below 1.31.0 as suspect rather than assuming a narrow window.

The Vulnerability Explained

The change in this project is a dependency bump, so the vulnerable logic lives in the SDK rather than in first-party code. What the lockfile records is the exposure:

-        "@modelcontextprotocol/sdk": "1.30.0",
+        "@modelcontextprotocol/sdk": "^1.31.0",

That one line is the whole finding: the build was pinned to an exact version on the vulnerable side of the fix.

The trust inversion

Walk the SDK's HTTP OAuth path as an attacker. You publish an MCP server at https://tools.attacker.example and get a developer — or an agent runtime acting on a user's behalf — to add it as a tool provider.

  1. The client calls your /mcp endpoint through StreamableHTTPClientTransport. You answer 401 with a WWW-Authenticate challenge that includes a resource_metadata pointer.
  2. The SDK's auth() flow follows that pointer and reads protected resource metadata. You control the document, so you declare an authorization_servers entry of https://as.attacker.example.
  3. The client fetches authorization server metadata from your endpoint and performs dynamic client registration against it. Your server now holds a client_id and, for confidential clients, a client_secret — issued by the client's own registration request, handed straight to you.
  4. The browser is redirected to your authorization_endpoint. Depending on the client's UX, the user sees a login page that looks like the service they expected to authorize.
  5. The client exchanges the returned code at your token_endpoint, sending its registered credentials and PKCE verifier along with it.

Nothing in that sequence requires breaking cryptography or guessing a secret. The attacker simply answers a question the client should never have asked a stranger: which authorization server should I trust for you?

Why PKCE and state do not save you here

It is tempting to assume PKCE blunts this. It does not. PKCE protects an authorization code from interception by a third party between a legitimate AS and the client. Here the attacker is the authorization server, so it receives the verifier itself. Likewise, state validation only proves the response came back to the same client session that started it — and it did. The controls that would have mattered are origin binding and an allowlist, neither of which is a cryptographic primitive.

Real-world impact

For a service embedding this SDK, the concrete losses are:

  • Dynamically registered client credentials disclosed to an attacker-run endpoint, usable to impersonate your client against any AS that honors the same registration.
  • Phishing with a legitimate-looking consent step. Because the client itself drove the redirect, the user has no obvious cue that the login screen is not the real identity provider.
  • Tokens minted by the wrong issuer. If downstream code accepts an access token without validating the issuer and audience against the resource it was requested for, the attacker's token can be replayed into your own request path.
  • Lateral movement in agent runtimes, where one agent may be connected to a dozen MCP servers and a single hostile entry sees the credential bootstrap for the connections it can influence.

The Fix

The remediation in this repository is the version move. The dependency declaration went from an exact pin on the vulnerable release to a caret range on the fixed one, and the lockfile was regenerated to resolve 1.31.0:

-        "@modelcontextprotocol/sdk": "1.30.0",
+        "@modelcontextprotocol/sdk": "^1.31.0",

Two things are worth calling out about this diff.

The range widened deliberately. Moving from 1.30.0 to ^1.31.0 means future 1.x releases install without a second lockfile edit. For a package shipping auth-flow security fixes, that is the behaviour you want; reproducibility is still guaranteed by the single resolved version recorded in the lockfile.

Most of the diff is npm metadata churn, not substance. Regenerating the lockfile with a different npm version dropped libc constraint arrays from a batch of optional, platform-specific dev dependencies. Those entries are build-tooling binaries and are unrelated to the vulnerability. Reviewers should not let that noise obscure the one line that matters — and should not treat those removals as part of the security change.

Upstream behaviour change. On the SDK side, 1.31.0 constrains which authorization server the OAuth flow will accept for a given MCP server, so a server can no longer nominate an arbitrary issuer and receive the client's credential bootstrap. The public surface — auth(), OAuthClientProvider, the HTTP transports — is unchanged, so this is a drop-in upgrade for application code.

Verify the whole tree, not just the top level. A direct bump does not guarantee a single copy. Confirm with:

npm ls @modelcontextprotocol/sdk

If a transitive dependency carries its own pinned 1.30.x, resolve it with an override or by upgrading that intermediate package.

Note that this change was not validated by an automated check against the project, so exercise your MCP client's OAuth path — a real 401 bootstrap against a server you control — before shipping.

Key Takeaways

  • An MCP server must never be the source of truth for which authorization server its client trusts. Versions of @modelcontextprotocol/sdk before 1.31.0 let server-supplied discovery data decide where credentials went; 1.31.0 binds that choice.
  • PKCE and state do not mitigate a malicious authorization server. When the attacker is the AS, it receives the code verifier and the state round-trips correctly. Only origin binding and an issuer allowlist help.
  • Dynamic client registration is a credential-emitting operation. Pointing it at an unvetted endpoint hands out a client_id/client_secret pair; treat the registration endpoint with the same suspicion as a token endpoint.
  • An exact pin is not a security posture. This project was pinned to 1.30.0, which froze it on the vulnerable side of CVE-2026-104850 until a human intervened. Pin for reproducibility, but pair it with advisory monitoring.
  • Separate the signal from lockfile noise. The libc array removals in this diff are npm regeneration artifacts; the security-relevant change is a single version string.

How Orbis AppSec Detected This

  • Source: MCP server-controlled OAuth discovery data — the WWW-Authenticate challenge returned by the MCP endpoint and the protected resource metadata it points to, including the advertised authorization_servers value, as consumed by the SDK's auth() flow.
  • Sink: the SDK's OAuth client requests to that nominated authorization server — dynamic client registration and the authorization-code/token exchange, which carry the client's client_id, client_secret, and PKCE verifier.
  • Missing control: no binding of the discovered authorization server back to the MCP server's own origin, and no operator-supplied allowlist of acceptable issuers, before credentials were transmitted.
  • CWE: unknown — no CWE has been assigned to CVE-2026-104850 at the time of writing.
  • Fix: upgrade @modelcontextprotocol/sdk from the pinned 1.30.0 to ^1.31.0, which constrains the authorization server the OAuth client will trust for a given MCP server.

Detection here was dependency-graph based: the vulnerable @modelcontextprotocol/sdk@1.30.0 was present in the resolved tree and matched the advisory for CVE-2026-104850. Reachability into the OAuth code path was not verified, so confirm whether your application actually invokes the HTTP OAuth flow when prioritising.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

CVE-2026-104850 is a textbook trust-direction bug dressed in modern plum

Prevention and further reading

Frequently Asked Questions

Does this affect an MCP client that only connects to MCP servers it runs itself over stdio?

Practically, no — the attack requires a server that can steer OAuth discovery, and the stdio transport does not perform the HTTP OAuth flow. The exposure is in HTTP-based clients using `OAuthClientProvider` with `StreamableHTTPClientTransport` or the SSE transport against servers you do not control.

Why did the dependency change from the exact pin `1.30.0` to the range `^1.31.0`?

The caret range lets patch and minor releases in the 1.x line install without another lockfile edit, which matters for a package shipping security fixes at this cadence; the lockfile still records one resolved version, so builds stay reproducible.

Is upgrading enough, or do MCP client authors also need to change their code?

Upgrading to `1.31.0` is the required step and needs no API change, but if your client supplies its own `OAuthClientProvider` or overrides discovery/metadata resolution, verify that your implementation also refuses authorization servers that the MCP server nominates outside your allowlist.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #20

Related Articles

high

SQLite Auth Database Plaintext Storage in InitAuthDB()

The `InitAuthDB()` function opened SQLite databases without restricting filesystem permissions, leaving bcrypt password hashes, session tokens, and user settings exposed to any local account with read access. The fix explicitly sets `os.Chmod(filepath, 0600)` immediately after database creation, ensuring only the owner can access authentication secrets.

high

undici 8.10.0 Cache Poisoning: CVE-2026-85152 Auth Bypass

undici, the HTTP client used by Node.js's `fetch()` implementation, shipped a caching layer that did not properly isolate cached responses by origin, letting a response poisoned on one origin be served to requests for another. This created a path to cross-origin authentication bypass, tracked as CVE-2026-85152 and fixed in undici 8.10.2.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.

high

PHP session_start() Missing Secure Cookie Flags in lasturl.php

The `session_start()` call in the last URL tracking component failed to set secure session cookie parameters, allowing session hijacking via man-in-the-middle attacks on HTTP connections or XSS exploitation. The fix configures `session_set_cookie_params()` with `httponly`, `secure`, and `samesite` attributes before starting the session.

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.