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:
- 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.
- 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.
- 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.
Summary
localDev()decides a request is local from theHostheader the client sends, not from where the connection actually comes from. On a self-hosted deployment that listens on0.0.0.0, that turns the "local development" strategy into an open door: anyone who can reach the port sendsHost: 127.0.0.1and getsLOCAL_DEV_SESSION_AUTH_CONTEXT.Details
dist/src/public/channels/auth.js(eve 0.24.4):req.urlis assembled from the incomingHostheader, sohostnameis 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, soplaceholderAuth()returnsnullinstead of throwing) started witheve start, which bindsoptions.host ?? "0.0.0.0". The defaultauth: [localDev(), vercelOidc(), placeholderAuth()]from the project template then authorizes any remote caller that spoofs the header: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:
Host.localDev()opt-in outside Vercel, e.g. require an explicitEVE_LOCAL_DEV=1or a constructor argument whenprocess.env.VERCELis unset.docs/guides/auth-and-route-protection.md:localDev()trusts the client'sHostheader, so a server that listens on anything other than loopback must not include it in theauthwalk.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.