Skip to content

Security Model

wody edited this page May 23, 2026 · 13 revisions

Security Model

vibe-coder is a single-user LAN tool. The threat model assumes:

  • The host PC is trusted (you own it).
  • The LAN is trusted (your home/office network behind a firewall).
  • The operator is trusted (you).
  • Public internet attackers cannot reach the server (no port forwarding, no public IP exposure).

If any of those assumptions fail, you need a reverse proxy with TLS, a WAF, and a higher-grade auth layer — beyond the scope of vibe-coder MVP.

Authentication

Bearer token + session cookie

POST /api/auth/login returns a token. Clients can pass it as Authorization: Bearer <token> or as a vibe_session cookie. Both paths converge in installAuth (Ktor plugin).

client → POST /api/auth/login {username, password}
       → 200 {token, deviceId, serverName, username}
       ← stores token in DataStore / cookie

client → GET /api/projects
       Header: Authorization: Bearer <token>
       → 200 [...]

Password storage

  • BCrypt cost 12 (configurable in server.yml).
  • Stored as hash only. The plaintext password is never persisted.
  • Timing-safe dummy verify runs on missing users (prevents enumeration).

Lock-out (v0.12.4+)

Two-layer brute-force defence:

  • Account lock: 10 consecutive /api/auth/login failures lock the account for 15 minutes.
  • IP block: 30 failures from a single IP within a 24-hour window block that IP for 24 hours. Catches credential-stuffing across multiple accounts (the per-account counter would otherwise reset between targets).

Both counters are in-memory (AuthService.failures / ipFailures); server restart clears them. The tradeoff is intentional — a server restart is itself a security-relevant event the operator initiates.

Timing-safe dummy verify runs on missing users so response time matches a real BCrypt verification (~250 ms at cost 12).

CSRF protection (v0.12.4+)

Every SSR POST carries a hidden _csrf input. The token is derived from the device cookie via HMAC-SHA256 with a server-side pepper (rotated on restart). REST API (Bearer header, not cookie) is exempt — Authorization headers can't be auto-attached by a cross-origin page.

Multipart uploads (/projects/{id}/files/upload, MCP secret file, etc.) carry _csrf in the query string since receiveParameters is unavailable.

See CsrfTokens.kt. Reject with HTTP 403 + audit log.

WebSocket Origin check (v0.12.4+)

WS handshake reads Origin and rejects mismatched hosts as a defense-in-depth against cross-site WebSocket hijacking — even though SameSite=Lax already prevents the cookie from attaching cross-site in the typical browser.

First-boot bootstrap

Two paths:

  1. Visit /setup in a browser → fill the form.
  2. Set VIBECODER_ADMIN_USERNAME and VIBECODER_ADMIN_PASSWORD env vars before first docker compose up -d. The entrypoint hands them to the server; if no admin exists yet, it auto-creates one.

After auto-bootstrap, change the password via /password so the plaintext doesn't linger in .env.

Path safety

Every disk read/write goes through PathSafety.normalizeAndCheck, which:

  1. Resolves .. and symlinks.
  2. Checks the result is a descendant of WorkspacePath.root (or .vibecoder/).
  3. Rejects with ApiException(400, "path_traversal", ...) if not.

Direct consequence: a malicious project name or file path cannot escape the workspace. Claude itself, when asked to "edit ../../../etc/passwd", hits the same check and reports failure to the user.

Upload blacklist

workspace:
  uploadDeniedExtensions:
    - exe
    - bat
    - cmd
    - ps1
    - sh

Configurable in server.yml. The default list blocks Windows scripts + shell scripts that could be accidentally executed by other tools.

WebSocket authentication

Auth happens via the first frame, not URL query:

{ "type": "auth", "token": "<token>" }

Sent within 5 s of connection, else server closes. This avoids leaking the token to access logs, proxies, or browser history.

Sandboxing

  • All Claude / Gradle / Git / npm child processes run as the unprivileged vibe user (UID/GID matched to host via PUID/PGID).
  • vibe has NOPASSWD sudo inside the container — but only inside the container. The container itself has no access to the host filesystem except the bind mounts in ./vibe-coder-data/.
  • No raw shell endpoint. There's no /api/exec or web terminal.
  • The script -q PTY wrap used for the Claude semi-automatic OAuth exposes only a single one-line text input field (the OAuth code), not a full shell.

Process lifecycle

  • All external commands have hard timeouts (5 min default for short ops, 10 min for git clone, 30 min for builds).
  • Cancellation calls Process.destroyForcibly() after destroy() waits 5 s.
  • Background tasks (build / install / clone) emit Done(status=…) over WebSocket on completion, including TIMEOUT/CANCELED states. Clients can always tell.

Network exposure

The container listens on 0.0.0.0:17880 inside its network. Compose maps it to ${VIBE_PORT:-17880} on the host. Do NOT forward this port to the public internet. If you need remote access:

  • VPN into your LAN (Tailscale / WireGuard).
  • Or expose only through a reverse proxy with HTTPS termination + auth.

Audit log (v0.15.0+)

Every IAM-level action (logins, password changes, project create/delete, build queue/cancel, MCP install, settings save, git token lifecycle, console new/cancel) is recorded into audit_log with timestamp, user, IP, result, and an optional JSON detail. View at /audit (left nav: 감사 로그).

See the Audit Log page for the schema, recorded actions, URL filter recipes, and rotation guidance. Failure to write an audit entry never blocks the user's request — it logs to stderr only.

Secrets in .env

.env is in .gitignore so it never reaches git, but it sits on disk in plaintext. Lock its permissions:

chmod 0600 ~/vibe-coder/.env

If you set VIBECODER_ADMIN_PASSWORD here for auto-bootstrap, rotate it via /password after first login, then either remove it from .env or accept the risk that someone with file read access can see it.

Reporting security issues

Open a private security advisory on GitHub. Do not file public issues.

Clone this wiki locally