[BUG] MCP isLoopbackBind() defaults to false when HOSTNAME is unset or bracketed, disabling Host validation and DNS rebinding protection

Open Beginner friendly
#2,244 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@saad-works is already working on this.

Since Nov 5, 2026.

  • #2251 by @saad-works — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript
Domain
security

Research direction

Start in src/lib/mcp/origin-policy.ts and compare its isLoopbackBind() with the implementation in bin/lib/launcher-utils.mjs. Check the existing tests for MCP origin policy and add cases for unset HOSTNAME, bracketed [::1], and other 127.0.0.0/8 addresses. Done when loopback requests run Host validation and untrusted Host headers are rejected.

Written by the indexing model from the issue text.

Description

bug

Description

In src/lib/mcp/origin-policy.ts, isLoopbackBind() checks whether the server is bound to a loopback interface by matching process.env.HOSTNAME against MCP_LOOPBACK_BINDS = new Set(["127.0.0.1", "::1", "localhost"]).

However, on default local installations (e.g., running npm start, bun run dev, or standalone node server.js without explicitly exporting HOSTNAME=127.0.0.1), process.env.HOSTNAME is undefined. It defaults to binding to 127.0.0.1, but isLoopbackBind() evaluates (process.env.HOSTNAME ?? "").trim().toLowerCase() to "", which returns false.

Consequently, in mcpOriginHostRefusal(request), line 59 (if (!isLoopbackBind()) return null;) exits early without running hostHeaderValidationResponse(request, mcpHostAllowlist()). The Host header validation is completely bypassed on default loopback instances, defeating the DNS rebinding protection designed for local MCP instances.

Furthermore, if an operator configures IPv6 loopback as bracketed HOSTNAME="[::1]" (documented in docs/DISTRIBUTION.md) or binds to another loopback address in 127.0.0.0/8 (such as 127.0.0.2 or 127.0.1.1), MCP_LOOPBACK_BINDS exact match fails and also returns false.

Steps to Reproduce

  1. Start LibreDB Studio locally without setting HOSTNAME in the environment (npm start or bun run dev). The server binds to 127.0.0.1:3000 by default.
  2. Send an MCP request to http://127.0.0.1:3000/api/mcp with an external Host header (simulating a DNS rebinding attack):
    curl -i -X POST http://127.0.0.1:3000/api/mcp \
      -H "Host: attacker-controlled-rebind.com:3000" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    
  3. Inspect the response.
  4. Notice that the request is not rejected by the Host header check (421 Misdirected Request or 403 with reason mcp_host_not_allowed), but passes directly to authentication because isLoopbackBind() returned false.

Expected Behavior

When running on loopback (including the default unset HOSTNAME, bracketed "[::1]", and any 127.0.0.0/8 address):

  • isLoopbackBind() should return true.
  • mcpOriginHostRefusal should execute hostHeaderValidationResponse(request, mcpHostAllowlist()).
  • Requests with untrusted or rebound Host headers should be rejected with mcp_host_not_allowed.

Actual Behavior

isLoopbackBind() returns false when HOSTNAME is unset or bracketed "[::1]". Host header validation is skipped, leaving local deployments unprotected against DNS rebinding.

Environment

  • LibreDB Studio Version: 1.2.0
  • Browser: All / Headless MCP
  • OS: macOS / Linux / Windows
  • Node.js/Bun Version: Bun 1.4.3 / Node.js 24
  • Database Type: All (MCP Server Layer)

Additional Context

bin/lib/launcher-utils.mjs correctly handles this in resolveBindAddress and isLoopbackBind:

export function resolveBindAddress({ host, libredbBind, hostnameEnv, systemHostname } = {}) {
  if (host) return host;
  const chosen = envText(libredbBind);
  if (chosen !== "") return chosen;
  const inherited = envText(hostnameEnv);
  if (inherited !== "" && inherited !== envText(systemHostname)) return inherited;
  return "127.0.0.1"; // defaults to 127.0.0.1
}

export function isLoopbackBind(hostname) {
  const host = envText(hostname)
    .toLowerCase()
    .replace(/^\[(.*)\]$/, "$1");
  return host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
}

src/lib/mcp/origin-policy.ts should follow the same pattern instead of an exact match against three literal strings with an empty fallback.

Possible Solution

In src/lib/mcp/origin-policy.ts, update isLoopbackBind():

export function isLoopbackBind(): boolean {
  const host = (process.env.HOSTNAME ?? "127.0.0.1")
    .trim()
    .toLowerCase()
    .replace(/^\[(.*)\]$/, "$1");
  return host === "" || host === "localhost" || host === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
}
Dominant language
TypeScript
Stars
810
Forks
158
Avg merge
10h 44m
Merged PRs (30d)
511

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from libredb/libredb-studio

All issues in libredb/libredb-studio

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.