Skip to content

fix(security): require Authorization header for streamable HTTP server [DEVX-806]#46

Open
geethanga-ct wants to merge 5 commits into
mainfrom
DEVX-806-insecure-auth-fallback-sse-mcp
Open

fix(security): require Authorization header for streamable HTTP server [DEVX-806]#46
geethanga-ct wants to merge 5 commits into
mainfrom
DEVX-806-insecure-auth-fallback-sse-mcp

Conversation

@geethanga-ct

Copy link
Copy Markdown
Contributor

Summary

Fixes the insecure auth fallback reported in DEVX-806 / Cure53 COM-15-004 (High). When the MCP server runs in remote/streamable HTTP mode (--remote=true), the /mcp handler treated the Authorization header as optional and fell back to the server's startup/system credentials. An unauthenticated network caller could therefore be served with the same privileges as the configured token.

This makes the streamable HTTP server a strict pass-through: every request must present its own commercetools bearer token, which is forwarded directly to the commercetools API. The startup credentials are no longer used to serve network requests.

What changed

  • streamable.ts
    • Added extractBearerToken() — structural validation of Authorization: Bearer <non-empty-token> (case-insensitive scheme).
    • POST /mcp returns 401 Unauthorized immediately when the header is missing/malformed, before any commercetools call — no fallback to config credentials.
    • Builds a per-request auth config with forced type: 'auth_token', so the caller's token is the one forwarded to the CT API. This also fixes a latent bug where a server started with client_credentials silently ignored the per-request header token.
    • Stopped mutating the shared this.authConfig (one request's token could leak into others); getServer() now accepts a per-request authConfig, threaded through stateless and stateful paths.
    • GET /mcp gets the same auth guard.
  • configuration.ts — added optional enforceAuthHeader?: boolean (defaults true) to IStreamServerOptions, so SDK embedders who handle auth via an injected server factory can opt out.
  • README.md — documented the mandatory Authorization: Bearer <token> requirement, the pass-through behavior, and the enforceAuthHeader opt-out.
  • Changesetminor bump for @commercetools/commerce-agent and @commercetools/commerce-mcp.

Migration

Clients connecting to a remote server (e.g. via mcp-remote) must now send an Authorization: Bearer <commercetools-access-token> header on every request. stdio mode is unaffected.

Testing

  • streamable suite: 28/28 pass (new cases: 401 on missing/malformed header, forwarded-token, enforceAuthHeader: false opt-out, GET auth).
  • Full agent test suite: 1627/1627 pass.
  • Lint + prettier clean; agent and processors packages build successfully.

End-to-end (reproduces the exploit)

# (A) No Authorization header -> 401 (was 200 + served with startup creds before fix)
curl -i -X POST http://localhost:8888/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}'

# (B) With Bearer header -> passes the gate; token forwarded to the CT API
curl -i -X POST http://localhost:8888/mcp \
  -H "Authorization: Bearer SOME_TOKEN" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}'

🤖 Generated with Claude Code

The remote/streamable HTTP MCP server no longer falls back to the
startup/system credentials when an incoming request omits the
Authorization header. Previously an unauthenticated network caller could
be served using the server's own configured token (Cure53 COM-15-004).

- Reject requests to /mcp with a missing/malformed Authorization header
  with 401, before any commercetools call is made
- Forward the caller's bearer token directly to the commercetools API
  (auth_token flow); also fixes a latent bug where a client_credentials
  startup silently ignored the per-request header token
- Stop mutating the shared authConfig; build an isolated per-request
  auth config instead
- Add enforceAuthHeader option (defaults true) so embedders that handle
  auth via an injected server factory can opt out

DEVX-806 / COM-15-004

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fe214b7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@commercetools/commerce-agent Major
@commercetools/commerce-mcp Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@geethanga-ct geethanga-ct requested a review from a team June 5, 2026 11:02
ajimae
ajimae previously approved these changes Jun 5, 2026
geethanga-ct and others added 2 commits June 9, 2026 10:00
…EADER env

Wire the streamable server's enforceAuthHeader opt-out through the CLI
entry point so standalone/containerized deployments can configure it.

- Parse --enforceAuthHeader and ENFORCE_AUTH_HEADER (default true, secure
  by default; only an explicit "false" disables enforcement)
- Pass the resolved value into CommercetoolsCommerceAgentStreamable
- Log a startup warning when enforcement is disabled in remote mode,
  since unauthenticated requests then fall back to startup credentials
- Document the env var as a transitional migration escape hatch for
  existing deployments that cannot add the Authorization header yet

DEVX-806 / COM-15-004

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
geethanga-ct and others added 2 commits June 12, 2026 15:15
This reverts commit 8577d79.

Per review feedback, the enforceAuthHeader opt-out should not be exposed
as an operator-facing CLI/env toggle on the published binary: disabling it
re-opens the exact fallback-to-startup-credentials behavior this change
fixes, and operators are the most likely to misuse it. The legitimate
"manage auth yourself" case is already served by the SDK constructor
option on CommercetoolsCommerceAgentStreamable (default true), which is
retained. This keeps the standalone server secure with no off-switch.

DEVX-806 / COM-15-004

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Requiring an Authorization header on every remote /mcp request is a
behavior-breaking change for existing remote clients, so release both
@commercetools/commerce-agent and @commercetools/commerce-mcp as major
(2.0.0 -> 3.0.0). Add an explicit BREAKING callout to the changelog entry.

DEVX-806 / COM-15-004

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@ajimae ajimae left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants