patchbay audit
·4 min read

Four response headers your MCP server probably forgot

An MCP server over HTTP is a web service. Any browser that can reach it inherits the same attack surface every other web service has. That surface is shaped by four response headers you can set in under an hour and most MCP servers I scanned did not.

The new HEADERS-001 check reads the response headers from the initialize probe and reports one finding per gap. Four gaps to know about.

Wildcard CORS plus credentials

If your server replies with Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true, every modern browser rejects the combination. The reason: it would let any page on the internet read authenticated responses from your server through the user's cookies. Browsers block it at the client, but a server that advertises it anyway is signalling that it did not think about this. Often the pairing sits under an authentication bypass that treats requests from any origin as trusted.

Access-Control-Allow-Origin: * on its own is milder. It opens the server to any browser origin that wants to read responses that do not carry credentials. That is fine for a fully public read-only endpoint and misleading for everything else.

No Strict-Transport-Security on HTTPS

HSTS tells browsers to never talk to your host over plain HTTP again. Without the header, the first connection a user ever makes to your MCP server over HTTPS is still open to a downgrade attack if someone can intercept it. The fix is one header on the first response:

Strict-Transport-Security: max-age=15552000; includeSubDomains

15552000 seconds is 180 days. Adjust to your taste; one week is the lower bound at which the protection is worth having.

No X-Frame-Options and no frame-ancestors

An MCP server that returns an error page or a status page can be embedded in a hidden iframe on an attacker's site. If the error page shows anything a signed-in user would see, a clickjacking overlay can steer them into actions on your site. The fix is either of:

X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

The check passes if either one is restrictive. A Content-Security-Policy whose frame-ancestors directive is literally * does not count; that is no restriction at all.

No X-Content-Type-Options

Without X-Content-Type-Options: nosniff the browser can MIME-sniff a response and treat it as a different type than the Content-Type header said. A JSON error body can be sniffed as HTML and script tags inside it can run. This is a one-line fix that costs nothing, so the check fires if the header is absent or not nosniff.

How the check runs

npx -y @patchbay-nebius/agent audit https://mcp.example.com/mcp

HEADERS-001 is passive. It uses the response headers that the initialize probe already captured, so no extra request goes to your server. Any gap aggregates into a single finding at medium severity with a bulleted evidence block, so your report does not fragment into four separate lines.

The check skips HSTS on plain-HTTP targets because TRANSPORT-001 already catches those. And it stays silent on servers that are not reachable at all.

What the check does not catch

Headers are shallow. A server can set all four correctly and still mishandle cookies, still accept a CSRF token on any origin, still serve mixed-content assets. HEADERS-001 catches the low-hanging fruit. For a thorough browser-side review you want a dedicated tool like securityheaders.com or Mozilla Observatory in addition. The point of patchbay is that one audit run touches the transport posture at all, so a server that misses the basics does not pass the eye by default.

One consequence: if your MCP server is called only by server-to-server clients and never by a browser, the four gaps carry less weight. The check still fires because it cannot tell from the outside who your callers are. Read the evidence block and decide.