# MCP vulnerabilities in 2026: real bugs and how to fix them

> Auth that failed open, tokens for the wrong service, commands run from config, SSRF and servers open to the network: 2026 MCP CVEs and the fix for each.

Source: https://devaiper.com/blog/mcp-security-vulnerabilities-2026
Published: 2026-10-16
Topics: MCP, Security, CVE, TypeScript

**Short answer:** September and October 2026 brought a wave of MCP advisories: an auth fallback that let anyone in (now on CISA's exploited list), SDKs accepting tokens issued for other services, a 9.9 remote code execution through server configs, SSRF in the reference fetch server, and an unauthenticated debugger server on 0.0.0.0. Upgrade the SDKs, then make auth fail closed, check token audiences, never run commands from config, block internal addresses and bind to localhost.

If 2025 was the year everyone installed MCP servers, autumn 2026 is the year the bills arrived. In a few weeks the advisories piled up: an authentication bypass now on CISA's list of exploited vulnerabilities, a CVSS 9.9 remote code execution, SSRF in the reference fetch server, and a stack of fixes in both official SDKs.

The good news: none of these are exotic. Every one is a classic web security mistake, wearing an MCP badge. This post walks through the bugs (all checked against NVD and the GitHub advisories on 16 October 2026), what each one teaches, and the TypeScript fix. If you want the risk overview first, start with [MCP security: the real risks](https://devaiper.com/blog/mcp-security-risks).

## The short version

| Bug | What went wrong | Fixed in | Lesson |
|---|---|---|---|
| LiteLLM, CVE-2026-59822 | Failed auth fell back to an empty user | 1.84.0 | Fail closed |
| Both SDKs, bearer auth | Accepted tokens issued for another service | TS 1.32.0 / 2.3.0, Py 1.30.0 / 2.2.0 | Check the audience |
| Langflow, CVE-2026-105697 | Ran any command from an MCP server config | 1.10.3 | Never run config as code |
| mcp-server-fetch, CVE-2026-104120 | SSRF through the `url` argument | 2026.10.10 | Block internal addresses |
| x64dbg-MCP, CVE-2026-107824 | No auth, listening on `0.0.0.0` | 1.1 | Bind to localhost, require auth |
| Both SDKs, OAuth client | Credentials sent to a server-chosen auth server | TS 1.31.0 / 2.2.0, Py 1.30.0 / 2.2.0 | Upgrade your clients too |

## Step zero: upgrade the SDKs

Several fixes landed in the official SDKs between late September and 15 October. For the TypeScript SDK they include the OAuth client sending credentials to an authorization server chosen by the MCP server ([CVE-2026-104850](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6qxp-vccf-f47h)), bearer authentication accepting tokens issued for another service ([GHSA-rvq5-wwqv-78pq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-rvq5-wwqv-78pq)), HTTP clients re-sending custom headers and bodies across cross-origin redirects ([GHSA-6prh-2h8m-c8cw](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-6prh-2h8m-c8cw)), missing request size limits ([GHSA-f897-62hf-6gxq](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-f897-62hf-6gxq)), and, in v2, shared transports delivering one HTTP client's messages to another ([GHSA-jm63-cq47-j5jp](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-jm63-cq47-j5jp)). The Python SDK fixed the same families of bugs.

| SDK | Upgrade to at least | Latest on 16 Oct 2026 |
|---|---|---|
| TypeScript v1: `@modelcontextprotocol/sdk` | 1.32.0 | 1.32.1 |
| TypeScript v2: `@modelcontextprotocol/server`, `/client` | 2.3.0 | 2.3.1 |
| Python: `mcp` | 1.30.0 on 1.x, 2.2.0 on 2.x | 2.3.0 |

```bash
npm ls @modelcontextprotocol/sdk @modelcontextprotocol/server
npm audit
```

Upgrading fixes the SDK. It does not fix the code you wrote around it, which is what the rest of this post is about.

## 1. Auth that fails open (LiteLLM)

Before 1.84.0, LiteLLM's MCP endpoint could be reached with a **fabricated** `Authorization` header. The fabricated header triggered an OAuth passthrough fallback, and the fallback replaced the failed key check with an empty user object, so the request reached MCP tools without a valid key ([advisory](https://github.com/BerriAI/litellm/security/advisories/GHSA-7488-6r32-c95q)). CISA added it to its [Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-59822) on 2 September 2026, which means attackers were already using it.

The lesson is older than MCP: **every failure path in authentication must end in "no".** Not a default user, not a guest, not a fallback. One function, one exit for success:

```ts
type Claims = { sub: string; aud: string | string[] };
type Verify = (token: string) => Promise<Claims>;

export async function authenticate(
  header: string | undefined,
  verify: Verify,
  audience: string,
): Promise<Claims | null> {
  const token = header?.match(/^Bearer (\S+)$/)?.[1];
  if (!token) return null;
  try {
    const claims = await verify(token);
    const aud = [claims.aud].flat();
    return aud.includes(audience) ? claims : null;
  } catch {
    return null; // never fall back to a default user
  }
}
```

`verify` is your JWT library's verification call (signature, expiry, issuer). The function returns claims or `null`, and nothing else.

## 2. Tokens issued for someone else (both SDKs)

Look at the `aud` line above. Both official SDKs shipped fixes because their bearer authentication accepted tokens that were valid, but **issued for a different service**. The [MCP security best practices](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices) are blunt: servers MUST NOT accept tokens that were not explicitly issued for them. A valid signature only proves someone issued the token. The audience proves it was issued **for you**.

## 3. Commands from config (Langflow, CVSS 9.9)

Before 1.10.3, Langflow launched whatever `command` and `args` a user put into an MCP server configuration, with no allowlist, wrapped in `bash -c`. Anyone who could edit MCP server settings could run any OS command on the host. With the default `LANGFLOW_AUTO_LOGIN=true`, an exposed instance handed out a token without credentials, so on a default setup no account was needed ([CVE-2026-105697](https://nvd.nist.gov/vuln/detail/CVE-2026-105697)).

An MCP stdio "server" is a program you run. A config field that names a program is code execution with extra steps. If your app lets users add servers, launch only configurations someone approved, and never through a shell:

```ts
export interface ServerConfig {
  command: string;
  args: string[];
}
const APPROVED: ServerConfig[] = [
  { command: "node", args: ["servers/repo-tools.js"] },
];
export const isApproved = (c: ServerConfig) =>
  APPROVED.some(
    (a) =>
      a.command === c.command &&
      a.args.length === c.args.length &&
      a.args.every((arg, i) => arg === c.args[i]),
  );
```

Then start it with `spawn(cfg.command, cfg.args, { shell: false })`. Compare the **whole** argument list: `node servers/repo-tools.js --eval=...` is not the approved server, and `npx -y some-package` is "run whatever that package is today." The spec's guidance for clients points the same way: show the exact command, untruncated, and get consent before running it.

## 4. SSRF in the fetch server

The reference `mcp-server-fetch`, up to release 2026.6.4, let the `url` argument point anywhere, including internal services and cloud metadata endpoints ([CVE-2026-104120](https://nvd.nist.gov/vuln/detail/CVE-2026-104120)). The 2026.10.10 release on PyPI adds a guard: it refuses private, loopback and link-local destinations unless you pass `--allow-private-ips`. (I checked the published package, not just the repository.)

The URL a tool receives is model output, and model output can be steered by [prompt injection](https://devaiper.com/blog/how-to-protect-ai-agents-from-prompt-injection). If your server fetches URLs, resolve the host and refuse internal addresses:

```ts
import { BlockList, isIP } from "node:net";
import { lookup } from "node:dns/promises";

const blocked = new BlockList();
for (const [net, bits] of [
  ["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10],
  ["127.0.0.0", 8], ["169.254.0.0", 16], ["172.16.0.0", 12],
  ["192.168.0.0", 16], ["224.0.0.0", 3],
] as const) blocked.addSubnet(net, bits, "ipv4");
for (const [net, bits] of [
  ["::", 127], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8],
] as const) blocked.addSubnet(net, bits, "ipv6");

function isBlocked(ip: string): boolean {
  const v4 = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1];
  if (v4) return blocked.check(v4, "ipv4");
  return blocked.check(ip, isIP(ip) === 6 ? "ipv6" : "ipv4");
}
```

```ts
export async function assertPublic(raw: string): Promise<URL> {
  const url = new URL(raw);
  if (!["http:", "https:"].includes(url.protocol)) {
    throw new Error(`blocked scheme: ${url.protocol}`);
  }
  const host = url.hostname.replace(/^\[|\]$/g, "");
  const addrs = isIP(host)
    ? [host]
    : (await lookup(host, { all: true })).map((a) => a.address);
  const bad = addrs.find(isBlocked);
  if (bad) throw new Error(`blocked address: ${host} -> ${bad}`);
  return url;
}
```

Then never let the HTTP client follow redirects on its own. A public URL that redirects to `169.254.169.254` is the oldest SSRF trick there is, and the spec's SSRF section lists redirect chains explicitly:

```ts
/** Fetch that re-checks every redirect hop instead of trusting it. */
export async function safeFetch(raw: string, maxHops = 3) {
  let url = await assertPublic(raw);
  for (let hop = 0; hop <= maxHops; hop++) {
    const res = await fetch(url, { redirect: "manual" });
    const next = res.headers.get("location");
    if (res.status < 300 || res.status > 399 || !next) return res;
    url = await assertPublic(new URL(next, url).href);
  }
  throw new Error(`too many redirects from ${raw}`);
}
```

The tests block, among others: `localhost`, `169.254.169.254`, `[::1]`, `[::ffff:127.0.0.1]` (IPv4 hiding in IPv6), `[fd00::1]`, `file:///etc/passwd`, and `http://2130706433/`, which is 127.0.0.1 written as a decimal number. The URL parser normalizes that one for you, which is exactly why you check the parsed host, not the string.

One honest limit: checking DNS and then fetching resolves the name twice, so a hostile DNS server can answer differently the second time (DNS rebinding). For servers that can reach sensitive networks, pin the resolved address or send all traffic through an egress proxy that enforces the same rules.

## 5. A debugger on the network (x64dbg-MCP)

Before 1.1, the x64dbg MCP plugin exposed every debugger tool over HTTP with **no authentication**, listening on `0.0.0.0` by default. Anyone who could reach the port could attach to processes, read and write memory, and write files anywhere ([CVE-2026-107824](https://nvd.nist.gov/vuln/detail/CVE-2026-107824)).

This is the exact scenario the spec's transport rules exist for: validate `Origin`, bind local servers to `127.0.0.1`, and authenticate every connection. With the `authenticate` function from section 1:

```ts
import { createServer, type IncomingMessage } from "node:http";

export function mcpHttpServer(verify: Verify, audience: string) {
  const origins = new Set(["http://localhost:3000"]);
  const ok = (req: IncomingMessage) =>
    !req.headers.origin || origins.has(req.headers.origin);
  return createServer(async (req, res) => {
    if (!ok(req)) return void res.writeHead(403).end();
    const auth = req.headers.authorization;
    const who = await authenticate(auth, verify, audience);
    if (!who) return void res.writeHead(401).end();
    res.writeHead(200).end(`hello ${who.sub}`); // MCP handler here
  });
}

mcpHttpServer(verify, "https://mcp.example.com")
  .listen(3000, "127.0.0.1"); // not 0.0.0.0
```

In the tests, a real server answers 401 with no token, with a forged token and with a valid token for another audience, 403 for a foreign `Origin`, and 200 only for a valid token issued for this server.

## A checklist for your MCP code this week

- [ ] SDKs at least at the versions in the table above, and `npm audit` clean.
- [ ] Every auth path ends in `null` except the one that verified the token **and** its audience.
- [ ] No server config field is ever executed unless it matches an approved command exactly.
- [ ] Any tool that fetches URLs blocks internal addresses and re-checks redirects.
- [ ] HTTP servers bind to `127.0.0.1` locally, check `Origin` and require auth.
- [ ] You know which MCP servers your team runs, and who updates them.

## How this was tested

All the TypeScript above was type-checked and run under Node 24's test runner: 7 tests covering the blocked and allowed addresses, a redirect to the metadata address, Node's `fetch` exposing the `Location` header with `redirect: "manual"`, the approved-command check, the auth function, and a real HTTP server on localhost. The vulnerability details come from NVD, the vendors' GitHub advisories and, for the fetch server, the published PyPI package. Advisories keep coming, so check the SDK security pages before you trust a "latest version" from any blog post, including this one.

Sources: [MCP security best practices (spec 2026-07-28)](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices), [TypeScript SDK advisories](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories), [Python SDK advisories](https://github.com/modelcontextprotocol/python-sdk/security/advisories), [CVE-2026-59822](https://nvd.nist.gov/vuln/detail/CVE-2026-59822), [CVE-2026-105697](https://nvd.nist.gov/vuln/detail/CVE-2026-105697), [CVE-2026-104120](https://nvd.nist.gov/vuln/detail/CVE-2026-104120), [CVE-2026-107824](https://nvd.nist.gov/vuln/detail/CVE-2026-107824).

## FAQ

### Which MCP SDK versions fix the 2026 advisories?

As of 16 October 2026: for the TypeScript SDK, @modelcontextprotocol/sdk 1.32.0 or newer on v1, and 2.3.0 or newer for the v2 packages. For Python, the mcp package 1.30.0 or newer on 1.x, or 2.2.0 or newer on 2.x. Check the advisories for anything published after that date.

### Is the official MCP fetch server safe to use?

Releases up to 2026.6.4 were affected by an SSRF vulnerability (CVE-2026-104120). The 2026.10.10 release on PyPI includes a guard that refuses private, loopback and link-local destinations unless you pass --allow-private-ips. Upgrade, and do not pass that flag on servers that can reach internal networks.

### What does it mean that an MCP CVE is on CISA's KEV list?

CISA's Known Exploited Vulnerabilities catalog lists flaws with evidence of exploitation in the wild. LiteLLM's MCP authentication bypass, CVE-2026-59822, was added on 2 September 2026. If you run LiteLLM's MCP endpoint, upgrade to 1.84.0 or newer.

### Why must an MCP server check the token audience?

A token issued for another service can be replayed against your server if you only check that it is valid. The MCP specification says servers must not accept tokens that were not explicitly issued for them, which means checking the audience claim.

### How do I protect an MCP server from SSRF?

Resolve the target host, refuse private, loopback, link-local and metadata addresses, and re-check every redirect hop instead of letting the HTTP client follow redirects. For full protection against DNS rebinding, pin the resolved address or send traffic through an egress proxy.

