flawopen.com/AI Security/mcp-typescript-sdk-bearer-token-audience-validation
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]
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]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-863
Step-by-Step Attack Flow
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]
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]
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
| Package | Affected versions | Fixed version |
|---|---|---|
@modelcontextprotocol/sdk | 1.6.0 through 1.31.0 | 1.32.0 or later |
@modelcontextprotocol/server | 2.0.0 through 2.2.0 | 2.3.0 or later |
@modelcontextprotocol/express | 2.0.0 through 2.0.1 | 2.0.2 or later |
@modelcontextprotocol/server-legacy | 2.0.0 through 2.3.0 | 2.3.1 or later |
Any package with its own aud validation in verifyAccessToken() | The documented missing-helper check may already be enforced by the application | Verify 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]
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.
};
},
},
});
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
- ✓Inventory server-side MCP HTTP integrations using
requireBearerAuth()orverifyBearerToken()and identify whether the authorization server issues tokens for multiple APIs.[1] - ✓Upgrade
@modelcontextprotocol/sdkto 1.32.0 or later; for 2.x use@modelcontextprotocol/server2.3.0 or later and@modelcontextprotocol/express2.0.2 or later when Express middleware is used; upgrade@modelcontextprotocol/server-legacyto 2.3.1 or later.[1], [4], [5] - ✓Set
expectedResourceto the exact resource identifier this MCP server is intended to serve; keep it in trusted server configuration rather than deriving it from the incoming request.[1], [2], [7] - ✓Make
verifyAccessToken()validate the token with the trusted authorization server and return its verified audience asAuthInfo.resource. If the issuer uses a bare non-URL audience identifier, validate it strictly inside the verifier and leaveexpectedResourceunset; do not guess or coerce it into a URL.[1], [2], [7] - ✓Test that a token for this resource succeeds, while a valid token for another service, a missing audience, and an audience with a different scheme, host, port, or path receive 401.[1], [2], [7]
- ✓Do not treat an SDK upgrade as sufficient:
expectedResourceis disabled unless configured, and enabling it without returningAuthInfo.resourcerejects every request.[1] - ✓Review access logs and token-validation telemetry for unexpected audiences; revoke or rotate credentials only when evidence or exposure assessment indicates a token may have been misused.[1]
Sources
- GitHub Security Advisory GHSA-rvq5-wwqv-78pq
- MCP TypeScript SDK v1.x pull request #2930: add expectedResource to requireBearerAuth
- MCP TypeScript SDK v2 pull request #2929: add expectedResource to bearer-token check
- MCP TypeScript SDK release notes for 1.32.0
- MCP TypeScript SDK release notes for 2.3.0
- MITRE CWE-863: Incorrect Authorization
- MCP TypeScript SDK server guide: bearer-token audience validation