Files
airship/SECURITY.md
T
Nayan 6c3d84f1d3 feat(server): loopback bind, host allowlist, and an origin gate
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
2026-08-16 13:05:37 +05:30

48 lines
2.7 KiB
Markdown

# Security
## Reporting a vulnerability
Please report vulnerabilities privately through
[GitHub security advisories](https://github.com/0xnyn/airship/security/advisories/new)
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.1` unless `--host` says
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-hosts` entries. 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 `Origin` does not match its `Host` is 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.0` exposes 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-Host` and `Forwarded` are 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-Policy` and `X-Frame-Options` headers are stripped** from the
surfaces airship serves (opt back in with `--keep-csp`); your deployed app's headers are
untouched by anything airship does.