Skip to content

localDev() trusts the client's Host header, so a non-loopback bind authorizes remote callers #1223

Description

@AndyShaman

Summary

localDev() decides a request is local from the Host header the client sends, not from where the connection actually comes from. On a self-hosted deployment that listens on 0.0.0.0, that turns the "local development" strategy into an open door: anyone who can reach the port sends Host: 127.0.0.1 and gets LOCAL_DEV_SESSION_AUTH_CONTEXT.

Details

dist/src/public/channels/auth.js (eve 0.24.4):

function localDev() {
  return (req) =>
    (process.env.VERCEL && process.env.VERCEL_ENV === "development") || isLoopbackRequest(req)
      ? LOCAL_DEV_SESSION_AUTH_CONTEXT
      : null;
}

function isLoopbackRequest(req) {
  let hostname;
  try { hostname = new URL(req.url).hostname } catch { return false }
  return LOOPBACK_HOSTNAMES.has(hostname) || LOOPBACK_IPV4_PREFIX.test(hostname) || hostname.endsWith(".localhost");
}

req.url is assembled from the incoming Host header, so hostname is whatever the caller claims — not the socket's address. The docs describe the helper as "Accepts requests addressed to a loopback hostname", which reads to most people as "requests that came from this machine".

Where it bites: a self-hosted agent (no VERCEL_ENV, so placeholderAuth() returns null instead of throwing) started with eve start, which binds options.host ?? "0.0.0.0". The default auth: [localDev(), vercelOidc(), placeholderAuth()] from the project template then authorizes any remote caller that spoofs the header:

# same server, honest Host → 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://<public-ip>:<port>/eve/v1/session -d '{}'
# 401

# same server, spoofed Host → 400, i.e. past auth and into body validation
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://<public-ip>:<port>/eve/v1/session \
  -H 'Host: 127.0.0.1:<port>' -H 'content-type: application/json' -d '{}'
# 400

Behind that route is a normal agent turn, which in a typical self-hosted setup means a host-native shell tool.

Suggestion

Any of these would remove the sharp edge, in rough order of preference:

  1. Decide locality from the connection, not the header — check the remote address of the socket (and treat a request as non-local when that information isn't available), instead of trusting Host.
  2. Make localDev() opt-in outside Vercel, e.g. require an explicit EVE_LOCAL_DEV=1 or a constructor argument when process.env.VERCEL is unset.
  3. At minimum, say it plainly in docs/guides/auth-and-route-protection.md: localDev() trusts the client's Host header, so a server that listens on anything other than loopback must not include it in the auth walk.

Happy to send a PR for whichever direction you prefer.

For context, I ran into this as a user of a self-hosted agent built on eve; the downstream project is fixing its side by binding to 127.0.0.1, but the strategy itself looks worth hardening for everyone else who deploys outside Vercel.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions