diff --git a/CLAUDE.md b/CLAUDE.md index 43f04fd5..608c9826 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,11 +187,13 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L |-------|---------| | **Auth** | Optional HTTP Basic via `CODEMAN_USERNAME` (defaults to `admin`) / `CODEMAN_PASSWORD` env vars. Active only when `CODEMAN_PASSWORD` is set (`middleware/auth.ts`) | | **Network bind** | Defaults to `127.0.0.1` (loopback). A non-loopback bind (`--host`/`CODEMAN_HOST`) without `CODEMAN_PASSWORD` **starts but warns loudly** (0.9.0; was fail-closed in COD-29/#107). `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1` acknowledges the warning. Classifier: `network-auth-policy.ts` | +| **Host guard** | Always-on Host-header allowlist blocks DNS rebinding (RCE on the default no-auth loopback install). Allows loopback, any IP literal, the bind host, `*.ts.net`/`*.trycloudflare.com`/`*.cfargotunnel.com`, the active managed tunnel, and `CODEMAN_ALLOWED_HOSTS`. ⚠️ **Custom reverse-proxy domains are rejected** unless added via `CODEMAN_ALLOWED_HOSTS=host,.suffix`. `registerHostGuard` in `server.ts`; policy in `network-auth-policy.ts` (`buildHostPolicy`/`isAllowedRequestHost`/`isAllowedRequestOrigin`) | +| **CSRF / Origin** | Always-on cross-site Origin guard rejects state-changing requests from foreign origins (covers self-update, session create/input, settings/tunnel toggles). **A missing Origin is allowed** so curl/CLI and Claude Code hooks keep working. The global body parser keeps `text/plain` RAW (no auto-JSON-parse, which had enabled simple-request CSRF); `/api/crash-diag` self-parses. WebSocket upgrade validates Origin+Host (anti-CSWSH) in `ws-routes.ts`. Added in `c669518` (closes 2026-06-09 review CRITICALs) | | **QR Auth** | Single-use 6-char tokens (60s TTL) for tunnel login. See `docs/qr-auth-plan.md` | | **Sessions** | 24h cookie (`codeman_session`), auto-extend, device context audit | | **Rate limit** | 10 failed auth/IP → 429 (15min decay). QR has separate limiter | | **Hook bypass** | `/api/hook-event` exempt from auth (localhost-only, schema-validated) | -| **Env vars** | `CODEMAN_MUX` (managed session), `CODEMAN_API_URL` (auto-set for hooks) | +| **Env vars** | `CODEMAN_MUX` (managed session), `CODEMAN_API_URL` (auto-set for hooks), `CODEMAN_ALLOWED_HOSTS` (extra Host/Origin allowlist entries for reverse proxies, comma-separated; bare `.suffix` matches subdomains) | | **Validation** | Zod schemas, path allowlist regex, `CLAUDE_CODE_*` env prefix allowlist | | **Headers** | CORS localhost-only, CSP, X-Frame-Options, HSTS if HTTPS | diff --git a/README.md b/README.md index 6e88561c..4f3f9bc7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
- Agent Visualization • Zero-Lag Input Overlay • Mobile-First UI • Respawn Controller • Multi-Session Dashboard + Agent Visualization • Zero-Lag Input • Autonomous Orchestrator • Respawn Controller • Mobile-First UI • Hardened Security
@@ -34,7 +34,7 @@ You'll need at least one AI coding CLI installed — [Claude Code](https://docs.
```bash
codeman web
-# Open http://localhost:3000 — press Ctrl+Enter to start your first session
+# Open http://localhost:3000 and start your first session
```
~/.claude/projects/*/subagents"]
+ TW["Team Watcher
~/.claude/teams/*"]
end
subgraph Persistence["Persistence Layer"]
@@ -505,14 +586,17 @@ flowchart TB
SM --> S1
SM --> S2
SM --> RC
+ SM --> ORC
SM --> SS
S1 --> RT
S1 --> SCR
S2 --> SCR
RC --> SCR
+ ORC --> SCR
SCR --> CLI
SW --> BG
SW --> SSE
+ TW --> SSE
```
---
@@ -537,13 +621,13 @@ The codebase went through a comprehensive 7-phase refactoring that eliminated go
| Phase | What changed | Impact |
|-------|-------------|--------|
| **Performance** | Cached endpoints, SSE adaptive batching, buffer chunking | Sub-16ms terminal latency |
-| **Route extraction** | `server.ts` split into 13 domain route modules + auth middleware + port interfaces | **−60%** server.ts LOC (6,736 → 2,697) |
-| **Domain splitting** | `types.ts` → 14 domain files, `ralph-tracker` → 7 files, `respawn-controller` → 5 files, `session` → 6 files | No more god files |
-| **Frontend modules** | `app.js` → 9 extracted modules (constants, mobile, voice, notifications, keyboard, CJK input, API, Ralph wizard, subagent windows) | **−24%** app.js LOC (15.2K → 11.5K) |
-| **Config consolidation** | ~70 scattered magic numbers → 9 domain-focused config files | Zero cross-file duplicates |
+| **Route extraction** | `server.ts` split into 15 domain route modules + auth middleware + port interfaces | **−67%** server.ts LOC (6,736 → 2,254) |
+| **Domain splitting** | `types.ts` → 16 domain files, `ralph-tracker` → 7 files, `respawn-controller` → 5 files, `session` → 6 files | No more god files |
+| **Frontend modules** | `app.js` → 18 extracted modules across infra, domain & feature layers | app.js core down to **~3.4K LOC** |
+| **Config consolidation** | ~70 scattered magic numbers → 10 domain-focused config files | Zero cross-file duplicates |
| **Test infrastructure** | Shared mock library, 12 route test files, consolidated MockSession | Testable route handlers via `app.inject()` |
-Full details: [`docs/code-structure-findings.md`](docs/code-structure-findings.md)
+Full details: [`docs/archive/code-structure-findings.md`](docs/archive/code-structure-findings.md)
---
diff --git a/docs/reports/security-review-2026-06-09.md b/docs/reports/security-review-2026-06-09.md
index 52e85f0f..191cea78 100644
--- a/docs/reports/security-review-2026-06-09.md
+++ b/docs/reports/security-review-2026-06-09.md
@@ -1,5 +1,16 @@
# Codeman Security Review — 2026-06-09
+> **⚠️ Remediation status (updated 2026‑06‑09):** the two CRITICALs and 5 of the 7
+> HIGHs below were **fixed the same day in commit `c669518` (shipped as 0.9.5)** —
+> an always‑on `Host`‑header + cross‑site `Origin` allowlist (`registerHostGuard`),
+> a raw `text/plain` body parser, a WebSocket `Origin`/`Host` check, and
+> HTML‑escaped subagent‑panel sinks. **The present‑tense "is exploitable" wording
+> below describes the pre‑fix v0.9.4 state.** Still open: **H2** (the self‑updater
+> trusts an unsigned git tag — needs signing infra) and dropping CSP
+> `'unsafe-inline'` (needs a nonce migration; H4's escaping already neutralises the
+> known XSS). Per‑finding breakdown in the *Implementation status* section below;
+> regression tests in `test/network-host-guard.test.ts`.
+
**Scope:** whole codebase (branch `master`, v0.9.4). Adversarial multi-agent review: 10 dimension specialists → diverse-lens skeptic verification of every finding (HIGH/CRITICAL got 3 independent refutation passes) → completeness-critic sweep. 47 raw findings → **25 survived verification** (+1 from the critic). 22 were refuted (mostly "already inside the OS trust boundary" same-uid claims and doc-accuracy nits). Several exploits were **confirmed live** with `curl` against throwaway test ports.
## TL;DR — the one thing that matters
diff --git a/docs/security-architecture.md b/docs/security-architecture.md
index 79637314..3b3b20bf 100644
--- a/docs/security-architecture.md
+++ b/docs/security-architecture.md
@@ -177,8 +177,52 @@ requests as local:
up (it does not gate the hook‑event exemption, but it gates everything else and
is the documented practice). Prefer `tailscale serve` (below), which authenticates
at the tailnet layer so untrusted clients never reach the loopback port at all.
-A future hardening could gate the hook‑event exemption on a shared secret while a
-tunnel is active.
+
+### Host‑header & Origin allowlist (DNS‑rebinding & CSRF defense)
+
+Since **0.9.5** an **always‑on** `onRequest` hook (`registerHostGuard`,
+`src/web/middleware/auth.ts`; policy in `src/web/network-auth-policy.ts`) runs
+**before** the auth pipeline in §2 and guards **every** request — including the
+localhost‑only exemptions above, SSE, the WebSocket upgrade, and static files. It
+closes the browser‑driven RCE path (DNS rebinding plus a cross‑site `text/plain`
+`POST`) that the loopback‑no‑password default otherwise exposed to any site the
+operator merely visits.
+
+- **Host allowlist (anti‑DNS‑rebinding).** The `Host` header is validated on
+ **every** request, all methods. A custom domain rebound to `127.0.0.1` is
+ rejected with `403 Forbidden: host not allowed` before any handler runs. Allowed:
+ `localhost`; **any** IP literal (IPv4/IPv6 — a browser hitting a numeric address
+ can't be a rebinding victim); the bind host; the suffixes `.ts.net`,
+ `.trycloudflare.com`, `.cfargotunnel.com`; the hostname of the active
+ Codeman‑managed tunnel; and anything in `CODEMAN_ALLOWED_HOSTS`. A missing/empty
+ `Host` is rejected.
+- **Origin / CSRF guard.** On **state‑changing** methods (everything except
+ `GET`/`HEAD`/`OPTIONS`) the `Origin` header must also pass the same allowlist,
+ else `403 Forbidden: cross‑site request blocked`. A **missing `Origin` is
+ allowed** (so `curl`, the CLI, and Claude Code hooks keep working); only a
+ present‑but‑foreign origin — or the opaque `null` origin (sandboxed iframe) — is
+ rejected. This blocks the cross‑site CSRF that could previously create sessions,
+ trigger self‑update, or flip `tunnelEnabled`.
+- **Raw `text/plain` bodies.** The global `text/plain` content‑type parser no
+ longer JSON‑parses bodies — it hands handlers the raw string (`/api/crash-diag`
+ self‑parses its beacon payload). This removes the CORS "simple request" CSRF
+ vector, where a cross‑site `fetch` with `Content-Type: text/plain` smuggled a
+ JSON body into a write route with no preflight — defense‑in‑depth alongside the
+ Origin guard.
+- **WebSocket upgrades.** The terminal WS upgrade (`src/web/routes/ws-routes.ts`)
+ runs the **same** Host + Origin check and closes with code `4003` on failure
+ (anti‑CSWSH).
+
+The policy is rebuilt per request from
+`buildHostPolicy(bindHost, tunnelManager.getUrl())`, so starting or stopping a
+tunnel at runtime updates the allowlist with no restart.
+
+> **Reverse‑proxy operators:** a custom proxy domain (e.g. `codeman.example.com`)
+> is **not** in the default allowlist and gets `403 host not allowed`. Add it via
+> `CODEMAN_ALLOWED_HOSTS` — comma‑separated, case‑insensitive; an exact hostname
+> matches only itself, while a leading‑dot entry (`.corp.internal`) matches the
+> bare domain **and** all subdomains. Behaviour is covered by
+> `test/network-host-guard.test.ts`.
---
@@ -342,6 +386,12 @@ production layout (`~/.codeman`, `-L codeman`, port 3000).
(CDN fallback for a few libraries). `script-src` and `style-src` additionally
allow `'unsafe-inline'` — relevant to the SVG/HTML handling in §5, where the
`octet-stream` + `nosniff` download (not the CSP) is what blocks execution.
+ Because `'unsafe-inline'` is still present (removing it needs a nonce
+ migration), AI‑derived strings rendered into the subagent/activity panels are
+ HTML‑escaped at the injection sites (`escapeHtml` in
+ `src/web/public/constants.js`; sinks in `panels-ui.js` / `subagent-windows.js`)
+ so a hostile tool name or argument can't execute — defense‑in‑depth from the
+ 2026‑06‑09 review (H4).
- `connect-src` allows `wss://api.deepgram.com` (streaming voice input).
- `img-src` allows `data:` and `blob:` (inline / generated images, QR codes).
- `frame-ancestors 'self'`.
@@ -367,6 +417,7 @@ production layout (`~/.codeman`, `-L codeman`, port 3000).
|------------|--------|
| `CODEMAN_PASSWORD` (+ `CODEMAN_USERNAME`) | Enable HTTP Basic auth |
| `--host` / `CODEMAN_HOST` | Bind host (default `127.0.0.1`) |
+| `CODEMAN_ALLOWED_HOSTS` | Extra `Host`/`Origin` allowlist entries for reverse proxies (comma‑separated; exact host, or leading‑dot `.suffix` for subdomains) — see §3 |
| `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK` | Acknowledge an unauthenticated non‑loopback bind (downgrades the warning) |
| `--https` | Enable TLS (adds HSTS) |
| `CODEMAN_INSTANCE` | Scope tmux socket + data dir for isolation |
@@ -379,9 +430,9 @@ production layout (`~/.codeman`, `-L codeman`, port 3000).
| Concern | File |
|---------|------|
-| Bind‑host classification, env‑flag parsing | `src/web/network-auth-policy.ts` |
+| Bind‑host classification, env‑flag parsing, Host/Origin allowlist (`buildHostPolicy` / `isAllowedRequestHost` / `isAllowedRequestOrigin`) | `src/web/network-auth-policy.ts` |
| Start‑and‑warn policy | `src/web/server.ts` (`WebServer.start()`) |
-| Auth pipeline, rate limiting, security headers, CORS | `src/web/middleware/auth.ts` |
+| Auth pipeline, rate limiting, security headers, CORS, Host/Origin guard (`registerHostGuard`) | `src/web/middleware/auth.ts` |
| File‑path containment (realpath‑before‑check) | `src/web/route-helpers.ts` (`validateSessionFilePath`) |
| File routes, caps, SVG handling, download blocklist | `src/web/routes/file-routes.ts` |
| Instance/socket/data‑dir scoping | `src/config/instance.ts` |