The proxy bound every interface — server.listen with no host — and the control WebSocket completed any upgrade with no Origin or Host check. That socket drives a coding agent with write access to the project, so anyone routable to the machine, or any page open in the developer's browser (WebSocket handshakes are not subject to CORS), could edit files, commit, and open PRs under the user's identity. Reported in #15 by yarikbright, with the repro this change's tests replay. Three checks, each stopping an attack the other two do not: - bind 127.0.0.1 by default (the posture opencode-server.ts always had) — stops the LAN attacker; --host / AIRSHIP_HOST opts out, with a loud launch warning that a wide bind is an unauthenticated agent - an exact-match Host allowlist (localhost and IP literals always pass; --allowed-hosts adds names) — stops DNS rebinding, where the attacker's Origin and Host match and an Origin check alone waves them through - Origin-matches-Host on every upgrade, control socket and HMR tunnel alike, refused with a real 403 before the upgrade completes. An absent Origin (curl, CLI) is allowed; Origin: null is not. The Host gate sits ahead of even the editor's own assets, so a blocked page cannot fetch overlay.js. requireHost accepts IPv6 literals bare or bracketed, EADDRNOTAVAIL now names the interface it could not find, and the printed URL follows the bind (wildcards present as localhost so it stays clickable). SECURITY.md states the model and its deliberate limits. The README's "runs entirely on localhost" is now an enforced default rather than an unchecked claim. Reported-by: yarikbright Closes #15
2.7 KiB
Security
Reporting a vulnerability
Please report vulnerabilities privately through GitHub security advisories rather than a public issue. If that form is unavailable to you, open an issue that says only that you have a security report and how to reach you — hold the details for a private channel.
Reports are welcome and credited. Issue #15 is what this policy grew out of.
The security model
Airship is a local development tool: an HTTP proxy in front of your dev server, a WebSocket
that drives a coding agent, and an overlay injected into your own app. The agent can write to
your project and, unless you pass --safe, do whatever your agent could do from a terminal.
What the server enforces, in packages/server/src/access.ts:
- Loopback bind by default. The proxy listens on
127.0.0.1unless--hostsays otherwise, so other machines cannot reach it at all. - A Host allowlist. Requests are served only for
localhost, IP literals, the configured--host, and exact--allowed-hostsentries. This is what stops DNS rebinding, where an attacker's page turns its own hostname into your loopback address; an IP literal cannot be rebound, which is why literals are always accepted. - An Origin gate on every WebSocket upgrade — the control socket and the HMR tunnel
alike. A handshake whose
Origindoes not match itsHostis refused before the upgrade completes, which stops cross-site WebSocket hijacking from another browser tab.Origin: null(sandboxed iframes,file://) is refused; an absent Origin — curl, CLI tools — is allowed, since it carries the same trust as any local HTTP client.
What is deliberately not defended
This is a reachability boundary, not authentication. Know what you are opting into:
--host 0.0.0.0exposes an unauthenticated agent. Anyone who can reach the interface can drive an agent with write access to your project — IP-literal Hosts are accepted by design, since refusing them would break exactly the LAN access you asked for. Airship warns at launch; use it only on networks where you trust every device.- Non-browser clients are not authenticated. Anything that can open a TCP connection to the (loopback-only, by default) port can speak the protocol.
X-Forwarded-HostandForwardedare never read. Behind a reverse proxy, add the public name to--allowed-hosts.- Host-less HTTP/1.0 requests are refused rather than guessed at.
- Upstream
Content-Security-PolicyandX-Frame-Optionsheaders are stripped from the surfaces airship serves (opt back in with--keep-csp); your deployed app's headers are untouched by anything airship does.