When MCP Meets OAuth: Common Pitfalls Leading to One-Click Account Takeover

PUBlished on
January 29, 2026
|
updated on
January 29, 2026

Fenix Qiao, Shuyang Wang

TL;DR

Security researchers at Obsidian Security discovered and responsibly disclosed multiple critical one-click account takeover vulnerabilities in Remote MCP servers deployed by several well-known organizations. These flaws put enterprise users at risk, potentially exposing sensitive SaaS data through MCP integrations.

The Root Cause: The MCP spec treats an MCP server as a resource server, but in practice most MCP servers are implemented as API wrappers in the form of a proxy, to be compatible with existing OAuth infrastructure. MCP servers therefore act as both an authorization server (AS) for MCP clients AND as a single OAuth client (with one shared static client_id) to the existing AS.

This added complexity led to common implementation flaws: MCP servers failed to properly handle consent and bind OAuth state to user sessions, enabling CSRF-style attacks where a malicious link can leak an MCP authorization code to an attacker-controlled redirect_uri. Once the attacker obtains that code, they can exchange it for tokens and act as the victim, invoking MCP tools with the victim’s granted permissions.

Disclosure & Impact: These vulnerabilities were reported in July–August 2025 and fixed by vendors in late September. This research represents the first public deep-dive on Remote MCP account takeover, offering insights to security researchers, bug bounty hunters, and MCP server developers.

MCP Spec Update: The MCP specification was updated on November 25, 2025 to include explicit guidance addressing these risks in its security best practices.

This post walks through the technical root causes, presents real-world case studies, and provides recommendations for building secure MCP OAuth flows.

Deep Dive Into MCP

MCP 101

As AI systems evolve from isolated chatbots to agents capable of real-world actions, the Model Context Protocol (MCP) emerged as a universal standard for connecting AI agents to external services, enabling portable and reusable integrations.

MCP servers expose "tools" (functions with structured schemas) that agents can discover by calling tools/list. Agents can then invoke these tools through a unified interface, abstracting away service-specific API implementations.

The MCP architecture has three components:

In a typical flow: The MCP client first connects to servers and discovers available tools. When a user asks an AI agent to perform a task, the LLM receives these tool schemas as context and decides which to invoke. The client formats the call according to the MCP protocol and sends it to the server. Results are returned through the client, where the LLM incorporates them into its response to the user.

New Protocol, New Attack Surface

As with any emerging technology, new security risks are inevitable.

In our previous research From well-known to Well-Pwned: Common Vulnerabilities in AI Agents, we uncovered how connecting to a rogue Remote MCP server can lead to Remote Command Execution (RCE) and account takeover (ATO) on the host side.

As more SaaS and cloud providers adopt MCP to expose their APIs to agents, securing these systems becomes paramount. This research focuses on an opposite threat: what happens when the MCP server itself is legitimate but its implementation is flawed?

Authentication is where complexity and vulnerabilities often emerge.

How MCP Handles Authentication

Note: The following references the MCP specification as of 2025-06-18, which was current during our research.

The spec recommends OAuth 2.1 for Remote MCP servers (HTTP-based transport).

As shown above, the MCP authentication code flow involves three parties: MCP Client, MCP Server, and Authorization Server (AS). The MCP server acts as a resource server and determines which AS to use for authentication.

When a MCP client attempts to access a protected resource without an access token, the MCP server returns a 401 Unauthorized. The MCP client then:

  1. Discovers the AS using OAuth 2.0 Protected Resource Metadata (RFC 9728) and Authorization Server Metadata (RFC 8414)
  2. Registers itself with the AS using Dynamic Client Registration (RFC 7591), if not already registered
  3. Redirects the user to the AS for authorization
  4. Obtains an access token and accesses the resource

This standard OAuth flow looks simple enough, right? Well, buckle up—things are about to get messy.

How Things Go Wrong

Ideal vs. Reality

Dynamic Client Registration (DCR) is essential to making this flow work. Since MCP clients connect to arbitrary MCP servers with different authorization servers, it’s impractical to pre-register everywhere. DCR lets clients register on-the-fly, obtaining a client_id and redirect_uri dynamically.

But here’s the problem: most SaaS providers deploy MCP servers as proxies to their own APIs and use their own authorization servers (AS) - which typically don’t support DCR. In testing 660 AS endpoints, only 27 (4%) actually supported it.

Why? DCR expands the attack surface. Without strong controls, it becomes a vector for abuse—spam, phishing, DoS. Unsurprisingly, most organizations would rather not blow up their working auth systems for the new kid on the block.

Also, the MCP spec explicitly forbids token passthrough.

MCP Server in the Middle

As David Wheeler famously said, “All problems in computer science can be solved by another level of indirection.”

To work around the issues described above, many MCP servers act as both an authorization server (AS) and an OAuth client simultaneously. They implement their own AS capabilities (including DCR) to serve MCP clients, while also acting as a pre-registered OAuth client to the SaaS AS. This creates two distinct OAuth layers: MCP clients see the MCP server as their AS, while the SaaS AS sees it as just another OAuth client.

The complete OAuth auth flow in this architecture works as follows:

OAuth Flow with the MCP Server Acting as a Proxy. © 2026 Obsidian Security

That’s a lot to digest. Let’s break it down step by step:

Pre-configuration:

  1. The MCP server operator manually registers with the SaaS AS and obtains a static client_id (e.g., mcp-proxy). This is a one-time setup.

Layer 1: MCP OAuth Flow (Start)

  1. AS discovery: The MCP client discovers that the MCP server itself is the AS, and retrieves its authorization and registration endpoints.
  2. Dynamic client registration: The MCP client performs DCR with the MCP server (AS), which returns a unique client_id.
  3. Authorization request: The MCP client redirects the user’s browser to the MCP server’s authorization endpoint with the dynamically registered client_id.

Layer 2: SaaS OAuth Flow

  1. 302 redirect: The MCP server constructs a new authorization request to the SaaS AS, using its static client_id (e.g., mcp-proxy) and a freshly generated state. To route the callback later, the server maps this state to the original MCP client’s client_id and redirect_uri - either by encoding them directly in the state, or by storing the mapping server-side.
  2. User consent: The SaaS AS displays a consent screen. The user reviews and approves the request.
  3. Consent tracking: The SaaS AS may remember the user’s consent for client_id=mcp-proxy (behavior varies by implementation).
  4. SaaS authorization code issuance: The SaaS AS generates an authorization code and redirects back to the MCP server.
  5. Token exchange (SaaS): The MCP server exchanges the SaaS authorization code for a SaaS access token.

Layer 1: MCP OAuth Flow (Complete)

  1. MCP authorization code generation: The MCP server generates its own authorization code and redirects to the MCP client.
  2. Token exchange (MCP): The MCP client exchanges the MCP authorization code for an MCP access token.

API Access: Using Both Tokens

  1. MCP requests: The MCP client makes requests to the MCP server using the MCP access token. The MCP server proxies these requests to the SaaS API using the SaaS access token.

Here, we assume the MCP server issues its own token, separate from the SaaS token. In practice, some implementations pass through the SaaS token directly - meaning both tokens are identical.

This auth flow is complex, and complexity is the enemy of security.

Round 1: The Shared Client ID Problem

From the SaaS AS’s perspective, all requests come from a single OAuth client: mcp-proxy. It has no visibility into the individual MCP clients behind it.

Once a user grants consent for mcp-proxy, the SaaS AS may remember that decision. On subsequent requests, even from a different MCP client, the AS sees the same client_id and skips the consent prompt.

This consent caching normally improves user experience, but in the proxy architecture, it becomes a security vulnerability.

Here’s the attack scenario:

  1. A victim connects to a legitimate MCP server and completes authorization.
  2. Later, an attacker registers a malicious MCP client with their own redirect_uri.
  3. The attacker sends a crafted authorization link to the victim.
  4. When the victim clicks, the SaaS AS sees the same client_id=mcp-proxy - no consent prompt appears.
  5. The MCP authorization code is issued and redirected to the attacker’s endpoint.

Mitigation 1: Consent at the MCP Server

Since the SaaS AS skips consent for the shared client_id, a natural solution is to add a consent layer at the MCP server.

Here’s how the fixed flow looks:

With this mitigation, the attack fails at the MCP server layer before reaching the SaaS AS. The victim sees a consent prompt for evil_client, raising suspicion and allowing them to reject.

Some MCP servers implement generic consent prompts without showing critical information like the client_name or redirect_uri. This creates a phishing risk where users blindly approve requests. A proper consent screen must display the client identity and destination to enable informed decisions.

Round 2: When Attacker Clicks "Approve" for You

Now we have two layers of consent - one at the MCP server level for each dynamic client, and another at the SaaS AS level for the shared client_id=mcp-proxy. Problem solved?

Not quite. We uncovered a new attack: the attacker completes the MCP consent themselves, captures the redirect URL to the SaaS AS, and sends it to the victim.

The full attack flow:

  1. The attacker registers a malicious MCP client and initiates the authorization flow.
  2. The attacker approves the MCP consent prompt themselves.
  3. The MCP server redirects to the SaaS AS - but instead of following, the attacker captures this URL.
  4. The attacker sends the captured URL to the victim.
  5. When the victim clicks, they land directly at the SaaS AS, skipping the MCP consent entirely.
  6. The SaaS AS issues a code (shared client_id, prior consent), and the MCP server redirects the MCP authorization code to the attacker’s endpoint.

Mitigation 2: State–Session Cookie Binding

At its core, this is a CSRF attack - the MCP server can’t tell who initiated the authorization request versus who completed it.

The fix? Use the same technique OAuth implementations have used for years to prevent CSRF: bind the state to a session cookie.

  1. When the user approves consent at the MCP server, set a session cookie in their browser
  2. Generate a state parameter and bind it to this session server-side
  3. When the SaaS AS callback arrives, verify that the state matches the value stored in the user’s session.
  4. If they don’t match, reject the request. No MCP authorization code is issued, and the attack fails.

Round 3: Cookie Injection Strikes Back

State-session binding deployed. Game over?

No, it’s effective against standalone CSRF attacks. But our research uncovered a bypass using chained vulnerabilities.

Here’s the catch: the MCP server has no user identity - the session cookie is anonymous. Anyone can trigger consent and generate a cookie. If an attacker can inject their cookie into the victim’s browser, they win.

How? Through cookie injection via:

Let’s take Wix’s MCP server as an example. The consent page sets a cookie: wix_code_verifier; Max-Age=600; Path=/; HttpOnly; Secure; SameSite=Lax.

Consider this scenario: the wix_code_verifier cookie has expired or doesn’t exist, but Wix’s AS remembers the user’s prior consent for the Wix MCP server. An attacker can inject their own cookie via any subdomain.

Here, we assume sto.wix.com is vulnerable to subdomain takeover. For local testing, we modify /etc/hosts to point this subdomain to our controlled IP.

How the attack works:

  1. Attacker starts their own authorization flow on the MCP server
  2. Captures the anonymous session cookie and AS authorization URL
  3. Crafts a malicious page that does two things:
    • Injects the attacker’s cookie into the victim’s browser
    • Redirects the victim to the captured AS URL
  4. Victim clicks the link. Their browser sends the SaaS authorization code and state to the MCP server - with the attacker’s injected cookie.
  5. The MCP server validates the state against the session (both from the attacker). Validation passes, and the MCP authorization code is issued to the attacker’s redirect_uri.

Mitigation 3: Consent Cookie Hardening

So how do we mitigate this? The answer lies in cookie security attributes. If using cookies to track consent decisions, use the __Host- prefix:

The __Host- prefix binds the cookie to the exact origin. This prevents cookie injection from subdomains and reduces the attack surface.

Real-World Cases

The vulnerabilities described above aren’t just theoretical — we discovered and responsibly disclosed multiple 1-click account takeover issues in production MCP servers. Below are two real-world case studies.

Case Study #1: Zero Consent Protection

(Identifying details redacted per program disclosure policy)

This MCP server did not implement consent. The SaaS AS had mcp-proxy configured as a trusted client with consent disabled. This gave attackers two options: hit the MCP server’s authorization endpoint, or skip it entirely and go straight to the SaaS AS.

Additionally, the dynamically registered MCP client’s client_id was identical to the MCP server’s client_id (mcp-proxy).

When redirecting to the SaaS AS, the MCP server embedded the MCP client’s redirect_uri in the state parameter.

Here’s a real vulnerable request:

Decoding the state parameter:

An attacker simply needed to:

  1. Construct a malicious AS authorization URL with their redirect_uri in the encoded state
  2. Send it to the victim
  3. Wait for the MCP authorization code to arrive at https://attacker.com/callback

No DCR required, no consent prompts, no user interaction beyond clicking a link.

To make matters worse, this MCP server used token pass-through - the MCP access token was a JWT containing the victim’s username and accessKey. The attacker could bypass the MCP server entirely and access the backend API directly.

Case Study #2: Square MCP Server

Square’s MCP server at mcp.squareup.com exposed tools for querying merchant data, processing transactions, and managing payment infrastructure. The implementation included MCP server–layer consent but still suffered from the consent bypass vulnerability described earlier.

The Vulnerability

The MCP server allowed dynamic client registration without redirect_uri restrictions, and used a shared static client_id (sq0idp-GHqvNvo8AgGE6akWuPdlRw) when redirecting to Square’s AS.

The authorization flow had two stages:

  1. User consent at mcp.squareup.com/authorize
  2. Redirect to connect.squareup.com/oauth2/authorize for SaaS authorization

An attacker could complete step 1 themselves, capture the redirect URL to connect.squareup.com, and send it directly to the victim - bypassing the MCP consent layer entirely.

Proof of Concept

Registered a test MCP client:

Then initiated a legitimate authorization flow to the MCP server:

After approving the MCP consent, the browser was redirected to Square’s authorization server:

Decoding the state parameter reveals the attacker’s client information:

This URL was captured and sent to the victim. When clicked:

  1. The victim’s browser went directly to connect.squareup.com, bypassing the MCP consent layer.
  2. Square’s authorization server recognized the existing consent for sq0idp-GHqvNvo8AgGE6akWuPdlRw, so no consent prompt appeared.
  3. An MCP authorization code was issued and redirected to test.pocs.app/callback.

After exchanging the MCP code for an MCP token, the make_api_request MCP tool could be used to execute API calls to Square, accessing merchant profiles, transaction history, bank account details, and more.

In the following demonstration, the victim uses the MCP Inspector to connect to the Square MCP server. The MCP server displays a consent screen, which the victim approves. Square’s AS recognizes that the victim previously authorized the Square MCP server and skips its consent prompt. Later, the victim clicks a malicious link prepared by the attacker (https://test.pocs.app/poc). Without any further interaction, the attacker obtains the victim’s MCP authorization code, exchanges it for a token, and successfully invokes Square APIs on the victim’s behalf.

Impact

Any user who had previously authorized the Square MCP server was vulnerable. A single click on a malicious link granted an attacker full access to the user’s Square account via the MCP server, with all previously granted permissions.

Interestingly, after this issue was reported, it was marked as a duplicate of an earlier report. The team deployed a fix by enforcing redirect_uri restrictions during Dynamic Client Registration (DCR). Unfortunately, this protection only applied to newly registered clients. Existing MCP clients registered before the fix remained vulnerable, allowing the bypass to still work.

Securing the MCP OAuth Flow

The OAuth proxy architecture used by Remote MCP servers creates a complex security boundary that’s easy to get wrong. As MCP adoption accelerates, these patterns will likely recur across implementations.

The good news? Effective defenses exist.

If you’re using the MCP-server-as-OAuth-proxy architecture, defense in depth is critical. Follow the latest MCP security best practices, which provides detailed guidance on consent flow implementation, OAuth state parameter validation, and consent cookie security.

Beyond the specification, we observed an effective additional defense deployed by real-world MCP servers: restrict redirect_uri registration during DCR, and only allow trusted patterns, for example localhost for development, custom schemes like cursor:// for native apps, or explicitly pre-approved domains beyond the attacker’s control.

It’s worth noting that one reason that existing MCP implementations are taking a proxy approach is the hurdle of DCR. With the latest spec update,  CIMD is now a more secure replacement of DCR, and we are anticipating to see more first party MCP built into SaaS applications, eliminating the double-token complication leading to above-mentioned vulnerabilities.

As of this write-up, CIMD adoption remains low - in our testing, only 3 out of 78 MCP authorization servers (less than 4%) supported it.

The MCP ecosystem is young, and the specification is still evolving. Early adopters are inevitably discovering these edge cases the hard way. But with each vulnerability disclosed and patched, we’re building a more secure foundation.

Frequently Asked Questions (FAQs)

You May Also Like

Get Started

Start in minutes and secure your critical SaaS applications with continuous monitoring and data-driven insights.

get a demo