Skip to content

fix(security): keep the local dev profile reachable from this machine only - #236

Merged
ttbombadil merged 1 commit into
mainfrom
fix/local-dev-loopback-only
Oct 9, 2026
Merged

ttbombadil merged 1 commit into
mainfrom
fix/local-dev-loopback-only

Conversation

@ttbombadil

Copy link
Copy Markdown
Collaborator

Problem

In the local dev profile (npm run dev:full), sketches run as native processes on the developer's machine. Two paths let other clients reach that:

  • LAN: Vite listened on all interfaces (server.host: true) and proxied /api and /ws to the loopback-only backend. Vite accepts every IP Host header. The local WebSocket verifier accepted an Origin equal to Host, and also any upgrade without Origin.
  • DNS rebinding: The backend checked no Host header. A page whose host name resolves to 127.0.0.1 could therefore use the backend on port 3000 from any website.

Change

  • vite.config.ts: server.host: "127.0.0.1".
  • Local mode, HTTP: createLocalHostGuard runs before every route and static file. It accepts only Host: localhost or a numeric loopback address (any port); everything else gets 403.
  • Local mode, WebSocket: the same host check runs in the upgrade verifier. An upgrade without Origin is accepted only from a loopback peer.
  • UNOSIM_UNSAFE_ALLOW_EXTERNAL_LOCAL_BIND=true keeps its explicit meaning and also lifts these checks.
  • Gateway mode, the Docker test bypass and the Docker/Compose production profile are unchanged.
  • Docs: docs/SECURITY.md and docs/INSTALL_LOCAL.md.

Tests

  • tests/server/security/access-control.test.ts:
    • LAN host via the dev proxy, DNS rebinding, LAN client without Origin.
    • HTTP host guard on a real listener.
    • Valid local browser and local tools.
    • Gateway mode and the unsafe opt-in are not restricted.
    • Replaces the old assertion that a LAN Origin/Host pair is allowed.
  • tests/server/vite-config.test.ts: Vite binds loopback.
  • Against the old code the new tests fail: the verifier accepted the LAN and rebinding upgrades.

Verification

  • npm run check, npm run test:unit (3045 passed), npm run check:docs, knip, git diff --check, ./run-tests.sh (11/11 incl. Docker, E2E, Sonar gate) all pass.
  • Live check with npm run dev:full:
    • Both listeners are on 127.0.0.1 only; the LAN IP refuses connections.
    • Host evil.example and the LAN IP: HTTP and WS get 403.
    • The localhost browser path via the Vite proxy (HTTP + WS) works.

🤖 Generated with Claude Code

… only

The Vite dev server of `npm run dev:full` listened on all interfaces
(`server.host: true`) and proxied /api and /ws to the loopback-only
backend, which runs sketches as native processes in local mode. Vite
admits every IP Host header, and the local WebSocket verifier accepted
an Origin equal to Host and any upgrade without Origin, so a LAN client
could start native sketch execution on the developer's machine. The
backend checked no Host header at all, so a DNS rebinding page whose
host name resolves to 127.0.0.1 could do the same from any website.

- Vite listens on 127.0.0.1 only.
- In local mode the backend accepts HTTP requests and WebSocket upgrades
  only for Host `localhost` or a numeric loopback address.
- A WebSocket upgrade without Origin is accepted only from a loopback peer.
- UNOSIM_UNSAFE_ALLOW_EXTERNAL_LOCAL_BIND keeps its explicit meaning and
  lifts these checks; gateway mode and the Docker profile are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ttbombadil
ttbombadil merged commit 090d6c0 into main Oct 9, 2026
6 checks passed
@ttbombadil
ttbombadil deleted the fix/local-dev-loopback-only branch October 9, 2026 12:10
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