FlawOpen

flawopen.com/AI Security/mcp-typescript-sdk-bearer-token-audience-validation

● CWE-863 · CVSS 6.8 · Medium
FlawOpen Security Research

MCP TypeScript SDK Bearer Tokens Lacked Audience Validation

GHSA-rvq5-wwqv-78pq affected MCP HTTP servers that accepted bearer tokens without checking their audience. Upgrade, then explicitly enable expectedResource and return the verified audience as AuthInfo.resource.[1]

💡 Plain English Explainer (ELI5)

A building guard accepts a valid badge but does not check which building the badge opens. If one badge office issues passes for several buildings, a pass for the neighboring building can get through. The MCP server must compare the badge's intended destination with its own resource URL.[1]

Core Concepts & Subsystem Terms

Audience (`aud`)
The resource or service for which an access token was issued. A valid signature or successful introspection does not by itself prove that the token was meant for this MCP server.[1], [7]
`expectedResource`
An opt-in setting added to the fixed SDK bearer-auth middleware. When set, it compares the configured server resource URL with the resource reported by the token verifier and rejects a mismatch with HTTP 401.[1], [2], [7]
`AuthInfo.resource`
The resource URL the verifier derives from trusted token-validation or introspection data. The patched middleware needs this value to enforce expectedResource; omitting it causes requests to be rejected when the check is enabled.[1], [2], [7]
Affected packages
The advisory covers @modelcontextprotocol/sdk 1.6.0–1.31.0, @modelcontextprotocol/server 2.0.0–2.2.0, @modelcontextprotocol/express 2.0.0–2.0.1, and @modelcontextprotocol/server-legacy 2.0.0–2.3.0. No CVE is assigned; the vendor maps the issue to CWE-863.[1], [4], [5]

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-863

Step-by-Step Attack Flow

Step 1

One issuer serves multiple APIs

The affected deployment uses an MCP HTTP bearer-auth helper, and its authorization server issues access tokens for more than one service. A token for another API can still be correctly signed and unexpired.[1]

Step 2

The MCP verifier validates the token but not its destination

In the affected integration, requireBearerAuth() / verifyBearerToken() had no built-in expected-resource check. If the application's verifyAccessToken() also omitted its own aud check, a token issued for a different service could be accepted by the MCP server.[1], [2], [3]

Step 3

The wrong service token reaches MCP authorization

A caller holding a valid token for another service may be treated as authenticated to this MCP server. The advisory rates the issue CVSS 3.1 6.8 and assigns CWE-863. It does not describe a specific in-the-wild incident.[1], [6]

Step 4

Upgrade and bind verification to this MCP resource

Use the patched release for the package line, set expectedResource to the resource identifier issued for this server, and have verifyAccessToken() return that verified audience as AuthInfo.resource. Upgrading alone leaves the new option off. If only the option is added and the verifier omits resource, all requests fail with 401.[1], [2], [3], [4], [5], [7]

Affected MCP TypeScript SDK package lines

PackageAffected versionsFixed version
@modelcontextprotocol/sdk1.6.0 through 1.31.01.32.0 or later
@modelcontextprotocol/server2.0.0 through 2.2.02.3.0 or later
@modelcontextprotocol/express2.0.0 through 2.0.12.0.2 or later
@modelcontextprotocol/server-legacy2.0.0 through 2.3.02.3.1 or later
Any package with its own aud validation in verifyAccessToken()The documented missing-helper check may already be enforced by the applicationVerify that the comparison is strict and uses the intended resource identifier

Source Code: Flaw vs. Secure Implementation

This follows the SDK's documented verifier shape and the merged fix. verifyJwt stands for the application's existing JWT or introspection validator. Before the patch, the helper offered no expectedResource setting and did not compare the audience; the verifier also returned no resource. The fixed middleware compares the configured resource with AuthInfo.resource and returns 401 for a different or missing audience. Use the exact resource identifier issued in the token's aud; the example selects only an audience URL on this server's origin, then lets the SDK compare the full resource. URL.canParse() and the URL parsing shown here follow the maintainer's current documentation. Because AuthInfo.resource is a URL, a bare non-URL aud value cannot be compared through expectedResource: validate that identifier strictly inside verifyAccessToken() and leave expectedResource unset for that deployment. Upgrade @modelcontextprotocol/sdk to 1.32.0 or later; on 2.x use @modelcontextprotocol/server 2.3.0 or later. Express integrations must also use @modelcontextprotocol/express 2.0.2 or later because earlier middleware does not pass the option through. The verifyJwt implementation is application-specific and intentionally omitted; no token forgery or exploit procedure is shown.[1], [2], [3], [4], [5], [7]

UNPATCHED FLAW
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';

const mcpServerUrl = new URL('https://api.example.com/mcp');
const auth = requireBearerAuth({
  verifier: {
    async verifyAccessToken(token) {
      const payload = await verifyJwt(token); // application JWT verification
      return {
        token,
        clientId: payload.sub,
        scopes: payload.scopes,
        expiresAt: payload.exp,
        // No resource is returned, and the affected helper had no expectedResource option.
      };
    },
  },
});
HARDENED SECURE PATCH
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';

const mcpServerUrl = new URL('https://api.example.com/mcp');
const auth = requireBearerAuth({
  verifier: {
    async verifyAccessToken(token) {
      const payload = await verifyJwt(token); // application JWT verification
      // `aud` may be a string, a list, or absent. Report this server's entry.
      const audience = [payload.aud ?? []].flat().find(
        aud => URL.canParse(aud) && new URL(aud).origin === mcpServerUrl.origin,
      );
      const resource = audience ? new URL(audience) : undefined;
      return {
        token,
        clientId: payload.sub,
        scopes: payload.scopes,
        expiresAt: payload.exp,
        resource,
      };
    },
  },
  expectedResource: mcpServerUrl,
});

Engineering & System Hardening Checklist

Sources

← AI Security Hub Directory →