Workflow Automation  ·  BraivIQ AI Engineering Playbook

MCP Server Authentication That Fails Closed: Lessons From The 29 October Advisory

MCP server authentication should fail closed: no key, no start, and no tool without a valid key. On 29 October 2026 an advisory for @sylphx/pdf-reader-mcp, CVE-2026-62264 fixed in 3.0.1, showed the opposite. In HTTP mode the server read its API key and logged that key authentication was enabled, but never checked the header, and it bound to all interfaces by default. This playbook builds an authentication wrapper, a start-up self-test that refuses to run if an unauthenticated request gets through, and a localhost default, in tested Python.

Published  ·  Updated  ·  9 min read  ·  By BraivIQ Engineering

A padlock on a white keyboard, illustrating MCP server authentication that fails closed for workflow automation

Key takeaways

  • CVE-2026-62264 (published 29 October 2026, CVSS 7.5, fixed in 3.0.1) affected @sylphx/pdf-reader-mcp 2.2.0 up to 3.0.1 when run as an HTTP server.
  • The server logged that API key authentication was enabled but never validated the X-API-Key header, and bound to 0.0.0.0 by default.
  • Refuse to start without a key, wrap every route with a constant-time key check and bind to localhost by default.
  • Add a start-up self-test that sends an unauthenticated request and refuses to run if it is accepted.

MCP server authentication is easy to believe in and easy to miss. The 29 October 2026 advisory for @sylphx/pdf-reader-mcp is a clear example: the logs said authentication was on, and it was not. The fix in your own servers is a habit as much as code. Test from the outside that an unauthenticated request is refused, every time the server starts.

What did the 29 October advisory disclose?

CVE-2026-62264, rated 7.5, affects @sylphx/pdf-reader-mcp from 2.2.0 up to but not including 3.0.1. When run as an HTTP server with MCP_TRANSPORT=http, it read the MCP_API_KEY environment variable and logged that API key authentication was enabled, but it never validated the X-API-Key header on incoming requests. The /mcp endpoint therefore accepted unauthenticated requests.

Combined with a default bind address of 0.0.0.0, the advisory says any client able to reach the port could invoke every PDF tool and read the contents of any PDF the server process could open on the host. URL-based sources also enabled limited server-side request forgery. Version 3.0.1 fixes it.

What does MCP server authentication look like when it fails closed?

MCP server authentication diagram: start-up requires a key and binds to localhost, a self-test sends an unauthenticated call that must get 401, every route checks the key in constant time, tools run only after a valid key, refused requests are logged with their source, a wider bind needs a named approval, and an access log records starts, refusals, calls and configuration changes.
If it logs that auth is on, test that it is. Tap to open full size.

How do you build it in Python?

mcp_auth.py
"""Fail-closed authentication for an HTTP MCP server, with a start-up self-test.

CVE-2026-62264 (@sylphx/pdf-reader-mcp, fixed in 3.0.1, published 29 October 2026): the server read
MCP_API_KEY and logged that key authentication was enabled, but never checked the header, and bound to
0.0.0.0 by default. Wrap every route, bind to localhost by default, and refuse to start if an
unauthenticated request gets through. Python 3.14 standard library only (wsgiref).
"""
from __future__ import annotations

import hmac
import os
from collections.abc import Callable, Iterable
from wsgiref.util import setup_testing_defaults

Start = Callable[[str, list[tuple[str, str]]], object]
App = Callable[[dict[str, object], Start], Iterable[bytes]]


def tools(environ: dict[str, object], start: Start) -> Iterable[bytes]:
    start("200 OK", [("Content-Type", "application/json")])
    return [b'{"tools": ["read_pdf"]}']


def require_key(app: App, key: str | None) -> App:
    if not key:
        raise RuntimeError("MCP_API_KEY is not set: refusing to start without authentication")
    def wrapped(environ: dict[str, object], start: Start) -> Iterable[bytes]:
        sent = str(environ.get("HTTP_X_API_KEY", ""))
        if not hmac.compare_digest(sent.encode(), key.encode()):
            start("401 Unauthorized", [("Content-Type", "text/plain")])
            return [b"unauthorised"]
        return app(environ, start)
    return wrapped


def call(app: App, headers: dict[str, str]) -> str:
    env: dict[str, object] = {}
    setup_testing_defaults(env)
    env.update({f"HTTP_{k.upper().replace('-', '_')}": v for k, v in headers.items()})
    status: list[str] = []
    app(env, lambda s, h: status.append(s))
    return status[0]


def self_test(app: App) -> None:
    if not call(app, {}).startswith("401"):
        raise RuntimeError("an unauthenticated request was accepted: refusing to start")


if __name__ == "__main__":
    os.environ["MCP_API_KEY"] = "sample-key-123"
    app = require_key(tools, os.environ.get("MCP_API_KEY"))
    self_test(app)
    print("no key:", call(app, {}), "| wrong key:", call(app, {"X-Api-Key": "nope"}), "| right key:", call(app, {"X-Api-Key": "sample-key-123"}))
    print("bind:", os.environ.get("MCP_BIND", "127.0.0.1"))
    try:
        require_key(tools, None)
    except RuntimeError as e:
        print("unset key:", e)

Python 3.14.4, standard library only. Run once as shown. Uses wsgiref test helpers, no network listener.

With the key set, the self-test passes and requests without a key or with the wrong key get 401, while the right key gets 200. The bind address defaults to 127.0.0.1. Without a key, the server refuses to start at all. The same wrapper pattern applies to any HTTP framework your MCP servers use.

What breaks in production?

  • **Logs that describe intent.** A log line saying auth is enabled proves nothing. The self-test proves it.
  • **New routes added later.** Wrap the application, not individual routes, so a new route is covered by default.
  • **Wide binds for convenience.** Binding to all interfaces should be a change with a named approval, not a default.
  • **Shared keys.** One key for every caller hides who did what. Prefer per-caller credentials and log the caller.

What are the alternatives to API keys for MCP servers?

OptionGood fitTrade-off
stdio transport, no networkLocal tools on one hostNo remote callers at all
API key with self-test, as aboveInternal HTTP serversKeys to rotate and protect
OAuth with audience checksShared or remote serversMore setup, covered in our MCP hardening playbook

Our playbook on MCP integration hardening covers OAuth issuer and audience checks, and our blog explains AI agent security for buyers.

Where should a team start?

Upgrade @sylphx/pdf-reader-mcp if you use it, then send an unauthenticated request to every MCP server you run and record the result. Our Workflow Automation London team adds this self-test to the workflows we build, listed on our services page, and a 14-day Proof Run includes it.

Frequently asked questions

What is CVE-2026-62264?

An advisory published on 29 October 2026 for @sylphx/pdf-reader-mcp. In HTTP mode it never validated the X-API-Key header, so the /mcp endpoint accepted unauthenticated requests. Version 3.0.1 fixes it.

How do you test that an MCP server requires authentication?

Send a request without credentials at start-up and refuse to run unless it is rejected with 401. Repeat the test in CI and after every configuration change.

Should MCP servers bind to all network interfaces?

Not by default. Bind to localhost unless a wider bind is needed, and make that a change with a named approval.

References

  1. GitHub Advisory Database, "@sylphx/pdf-reader-mcp: HTTP transport accepts unauthenticated requests despite a configured MCP API key (CVE-2026-62264)", 29 October 2026. https://github.com/advisories/GHSA-q344-5v34-gm84