fix: enforce Host allow-list on HTTP transport to block DNS rebinding#340
Merged
Conversation
The HTTP transport's DNS-rebinding guard only checked that the request Origin hostname equalled its Host hostname. That is self-consistency between two attacker-controlled values, not a trust decision: after DNS rebinding a malicious page sends requests where both headers carry the attacker hostname, so the check passed and the browser reached /mcp and could execute SQL against the configured database. Replace the equality check with an explicit Host allow-list (the real rebinding defense), validated on every request — including those with no Origin, since a rebound same-origin POST may omit it. Origin, when present, must also be on the list so an untrusted origin is never reflected into Access-Control-Allow-Origin. The allow-list always includes loopback. On a wildcard bind (0.0.0.0/::) the machine's own hostname and external IPs are auto-allowed via getSelfHosts(), so local and by-IP access keep working with no config; this is safe because a rebound request carries the attacker's hostname, never the victim machine's own name/IP. Operators add other names (e.g. a reverse-proxy/public DNS name) with --allowed-hosts / DBHUB_ALLOWED_HOSTS, or "*" to disable the check when fronted by their own auth/proxy. The resolved list is logged at startup. Closes GHSA-fm8p-53ww-hf6w, GHSA-fp99-xwp4-hv8q, GHSA-qvg2-3c48-77mx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens the HTTP transport against DNS rebinding by replacing the prior Origin↔Host equality check with an explicit Host allow-list (and ensuring Origin, when present, is also allow-listed) to prevent browsers from reaching /mcp under attacker-controlled hostnames.
Changes:
- Add host allow-list construction (
buildAllowedHosts) with loopback defaults, optional operator overrides (--allowed-hosts/DBHUB_ALLOWED_HOSTS), and wildcard disable (*). - Enforce allow-list validation in the HTTP middleware for every request (including requests without
Origin). - Add unit tests for allow-list behavior and update docs/config examples.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/cross-origin.ts | Implements allow-list construction, self-host discovery, and Host/Origin validation logic. |
| src/utils/tests/cross-origin.test.ts | Adds tests for allow-list behavior, self-host discovery, and CVE-shaped cases. |
| src/server.ts | Integrates allow-list validation into HTTP middleware and logs the active allow-list at startup. |
| src/config/env.ts | Adds --allowed-hosts / DBHUB_ALLOWED_HOSTS parsing and validation. |
| README.md | Documents the new DNS-rebinding protection behavior and how to unblock reverse-proxy hostnames. |
| docs/config/command-line.mdx | Adds a dedicated --allowed-hosts section explaining behavior and usage. |
| CLAUDE.md | Updates configuration flag documentation to include --host and --allowed-hosts. |
| .env.example | Documents DBHUB_ALLOWED_HOSTS and how it affects Host allow-listing. |
… test - normalizeHost(): reject crafted authority strings (e.g. "evil.com/foo", "evil.com@host") via INVALID_HOST_CHARS, mirroring Host-header validation so an operator typo cannot silently broaden the allow-list. Add a test. - Capture getSelfHosts() once in the round-trip test (OS interface enumeration can change between calls → flaky). - Docs/.env.example: list loopback IPv6 as [::1] to match the bracketed form the allow-list actually compares against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/server.ts:202
req.headers.originis typed asstring | string[] | undefined(Node header values can be arrays), butvalidateOrigin()expectsstring | undefined. This will failtsc --strictand can also lead to reflecting an array value intoAccess-Control-Allow-Originat runtime. Normalize the header to a single string (and treat non-string values as malformed) before validation and reflection.
app.use((req, res, next) => {
const origin = req.headers.origin;
const result = validateOrigin(origin, req.headers.host, allowedHosts);
if (!result.ok) {
return res.status(result.status).json({
error: result.status === 400 ? 'Bad Request' : 'Forbidden',
message: result.message,
});
}
// CORS headers — only reflect validated origins
res.header('Access-Control-Allow-Origin', origin || 'http://localhost');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
tianzhou
added a commit
that referenced
this pull request
Jun 24, 2026
Release containing the HTTP-transport DNS-rebinding fix (#340). Merging this to main triggers the npm trusted-publish workflow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
The HTTP transport's DNS-rebinding guard only checked that the request
Originhostname equalled itsHosthostname — self-consistency between two attacker-controlled values, not a trust decision. After DNS rebinding, a malicious page sends requests where both headers carry the attacker hostname, so the check passed, the browser reached/mcp, and could execute SQL against the configured database with no auth, no prompt injection, and no model involvement.This replaces the equality check with an explicit Host allow-list (the actual rebinding defense) and closes three reported advisories that all share this root cause:
GHSA-fm8p-53ww-hf6wGHSA-fp99-xwp4-hv8qGHSA-qvg2-3c48-77mxWhat changed
Hostagainst an allow-list on every request — including requests with noOrigin, since a rebound same-origin POST may omit it (an Origin-gated check would miss that).Origin, when present, must also be on the list — so an untrusted origin is never reflected intoAccess-Control-Allow-Origin.0.0.0.0/::), the machine's own hostname and external IPs are auto-allowed (getSelfHosts()), so local and by-IP access keep working with zero config.--allowed-hosts/DBHUB_ALLOWED_HOSTSfor other names (e.g. a reverse-proxy / public DNS name);"*"disables the check for users behind their own auth/proxy. Resolved list is logged at startup.Why auto-allowing self IPs is safe
A rebound request always carries the attacker's hostname in
Host(the URL-bar name), never the victim machine's own IP/name — so those entries can't match an attack. A direct cross-origin fetch to the IP still carries a foreignOriginand is rejected.Verification (built server,
--host 0.0.0.0 --demo)Hostevil.attacker.test(+matching Origin)localhost/127.0.0.1localhost:5173→ backendBehavior change to note in release notes
A client reaching DBHub via a reverse-proxy or custom DNS name now needs
--allowed-hosts <name>(the403message says exactly this). Local access, by-IP access, and the bundled workbench are unaffected.Tests & docs
cross-origin.test.ts(self-host + CVE-shape cases); full unit suite green;tscclean on changed files; build passes.docs/config/command-line.mdx(new--allowed-hostssection),README.md,.env.example,CLAUDE.md.Out of scope
Bearer-token auth for
/mcp(separately tracked, issue #66). The bind default stays0.0.0.0; the Host allow-list makes the security boundary correct regardless of bind address.🤖 Generated with Claude Code