Skip to content

fix(server): refuse open-in-editor when not bound to loopback - #2

Draft
jscheid wants to merge 3 commits into
fix/heartbeat-refcountfrom
fix/open-in-editor-guard
Draft

fix(server): refuse open-in-editor when not bound to loopback#2
jscheid wants to merge 3 commits into
fix/heartbeat-refcountfrom
fix/open-in-editor-guard

Conversation

@jscheid

@jscheid jscheid commented Sep 4, 2026

Copy link
Copy Markdown

Stacked on #1. The diff below is only this branch's own changes; review #1 first.

POST /api/open-in-editor reads command and argsTemplate from the request body and passes them to spawn, so anything able to reach the port executes code as the server's uid. DIFIT_EDITOR=none did not close it, because the check consulted the caller's own editor.id before the environment.

This matters now because the branch above this one requires binding 0.0.0.0 so a reverse proxy can reach the server, which puts that endpoint in front of every neighbour on a flat container network.

What this adds

  • A pure classifier for loopback addresses, built on node:net's isIP and BlockList rather than string matching.
  • Two guards at the top of the handler, before the request body is even parsed: refuse to spawn when the server is not bound to loopback, and honour DIFIT_EDITOR=none / EDITOR=none from the environment regardless of what the caller claims.
  • The startup warning now uses the same classifier, so it stops claiming "accessible from external network" for a server that is not.

This is the narrow fix, not an authentication system. Resolving the spawn spec from server-side configuration instead of the request body remains the real fix and is tracked separately.

Notes for the reviewer

  • The classifier reads the address the socket actually bound to, not the --host string. Node's listen expands abbreviated forms through getaddrinfo that isIP rejects, so --host 127.1 binds loopback while classifying as external — over-blocking, and a warning that lies.
  • isLoopbackHost returning true means the guard does not fire. Over-classifying widens what is permitted; under-classifying merely over-blocks. An indeterminate bound address is treated as not loopback.
  • Environment precedence lives in exactly one helper that returns both the value and which variable supplied it, so the guard cannot drift from the resolution it guards. A blank DIFIT_EDITOR counts as unset, otherwise it masked EDITOR=none and silently re-enabled the spawn.
  • No test lets a real command execute: the fixture command is a path that cannot exist, and every test asserts the exact error string so it pins the guard it targets rather than any rejection.

Verification

pnpm test 929 passed / 2 skipped · pnpm check clean · pnpm build clean. Run under the pinned toolchain (mise exec -- pnpm …); Node 26 produces ~70 spurious localStorage failures against this repo's happy-dom/vitest pairing.

Classifies a bound address as loopback-only using node:net isIP and
BlockList, so IPv4-mapped and long-form IPv6 loopback are recognised and
malformed values are not.
DIFIT_EDITOR takes precedence over EDITOR, and blank values count as
unset. Returning the source alongside the value keeps the guard and the
request resolution from encoding that order separately.
The handler spawns a command taken from the request body, so anything
reaching the port ran code as our uid. The guards consult the address the
socket actually bound to, and honour the environment ahead of the
caller-supplied editor id.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant