fix(http): require auth and bind loopback for HTTP transport (RG-4626) - #85
Merged
Conversation
The --transport=http server bound to all interfaces, exposed POST /mcp with no authentication or Host/Origin validation, and dispatched every tool under the operator's process-wide HUB_PAT_TOKEN. Any TCP peer, or a website the operator visited (DNS rebinding), could therefore act as the operator on Docker Hub, including creating and modifying repositories (CWE-306, CWE-346/CWE-350; CVSS 7.4). Harden the HTTP transport to match the accepted upstream pattern (docker/mcp-gateway, GHSA-46gc-mwh4-cc5r): - Bind to 127.0.0.1 by default; expose deliberately with --host. - Require a bearer token (MCP_AUTH_TOKEN) on every /mcp request and fail closed at startup unless --allow-unauthenticated is explicitly passed. Token comparison is constant-time. - Add a DNS-rebinding/CSRF guard: reject disallowed Host headers and any browser Origin not in --allowed-origins. Non-browser MCP clients, which send no Origin and a loopback Host, are unaffected. Also surface fatal startup errors synchronously on stderr (the async logger was truncated by process.exit, hiding the fail-closed reason), and add an integration test suite plus a CI step covering all the above. stdio transport behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the `/^Bearer\s+(.+)$/i` Authorization parser with linear indexOf/slice parsing to remove the polynomial-ReDoS exposure on the attacker-controlled header (CodeQL js/polynomial-redos). - Sanitize attacker-controlled Host/Origin header values (strip control chars incl. CR/LF) before logging rejections, preventing forged/split log entries (CodeQL js/log-injection). Behaviour is unchanged for well-formed requests; tests still pass (8/8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Addressed the CodeQL findings in c84d09e:
Behaviour is unchanged for well-formed requests; the test suite still passes 8/8. CodeQL should clear alerts 46–48 on the next scan. |
The previous sanitizer stripped control characters via a single Unicode-range replace, which CodeQL did not recognize as a log-injection barrier (alerts 49/50 re-fired on the Host/Origin log lines). Lead with a newline-stripping replace -- the same pattern already used for the request body elsewhere in this file and accepted by CodeQL -- then keep the control-character strip as defense in depth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Follow-up in 5392a36: the ReDoS alert (46) cleared, but the log-injection alerts re-fired (49/50) because CodeQL didn't recognize the Unicode-range strip as a barrier. |
vdamery
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes RG-4626 (CWE-306 Missing Authentication + CWE-346/CWE-350 DNS rebinding; CVSS 7.4, High). Reported privately to security@docker.com.
In
--transport=httpmode the server was a confused deputy:app.listen(port)with no host) → reachable by any TCP peer.POST /mcphad no authentication and no Host/Origin validation.HUB_PAT_TOKEN, including write tools (createRepository,updateRepositoryInfo).So an uncredentialed caller — directly over the network, or via DNS rebinding from a website the operator visited — could enumerate private repos and create/modify repositories under the operator's Docker Hub identity.
Changes
Hardened the HTTP transport to match the accepted upstream pattern (
docker/mcp-gateway, GHSA-46gc-mwh4-cc5r). stdio transport is unchanged.127.0.0.1; expose deliberately with--host=0.0.0.0(logs a warning).MCP_AUTH_TOKENis set (clients sendAuthorization: Bearer <token>) or--allow-unauthenticatedis explicitly passed. Token comparison is constant-time (crypto.timingSafeEqualover SHA-256 digests).Hostheaders (allowlist: loopback +--host+--allowed-hosts) and any browserOriginnot in--allowed-origins. Non-browser MCP clients (noOrigin, loopbackHost) are unaffected.process.exit, hiding the fail-closed reason).MCP_AUTH_TOKENenv,--host,--allowed-hosts,--allowed-origins,--allow-unauthenticated. Documented in the README ("Securing the HTTP transport").Tests
Added
src/server.test.ts(Node built-in test runner, compiled viatsconfig.test.json— no esbuild/tsx dependency so it runs under the repo'signore-scriptsCI) and wirednpm testinto the Lint workflow. Coverage:401on missing and wrong bearer token403on spoofedHost(DNS rebinding) and disallowed browserOrigin200on an authenticated loopback request--allow-unauthenticatedserves without a tokenOriginis honoredLocal:
build,lint,format:check,test(8/8) all green.Follow-up
Per the ticket's coordinated-disclosure note: once merged, publish a GHSA/CVE with the security team.
🤖 Generated with Claude Code