diff --git a/CLAUDE.md b/CLAUDE.md index 1436f9ff..7005deba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -261,6 +261,50 @@ sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168 Single source of truth: `neode-ui/public/assets/img/app-icons/` Naming: `{app-id}.{png|webp|svg}` — do not duplicate elsewhere. +## Security Standards (Post-Pentest — Mandatory) + +These rules come from a full penetration test (33 findings, all remediated). Follow them for ALL new code. + +### Backend (Rust) + +- **Backend binds to 127.0.0.1 ONLY** — never `0.0.0.0`. All external access goes through nginx. +- **Validate ALL user input before path construction** — reject `..`, `/`, `\`, null bytes. Use the existing `validate_app_id()` pattern in `tor.rs`. +- **Never pass user input to `sudo` commands** — if unavoidable, validate strictly against an allowlist of characters `[a-zA-Z0-9_-]`. +- **Every HTTP endpoint that returns sensitive data MUST check authentication** — use `self.is_authenticated(&headers).await` or be in `UNAUTHENTICATED_METHODS` with justification. +- **Rate-limit authentication endpoints** — `extract_client_ip()` must only trust `X-Real-IP` from the loopback interface (127.0.0.1). +- **Federation messages require ed25519 signatures** — never accept unsigned peer-joined messages. +- **RBAC: use explicit allowlists, not prefix matching** — `method.starts_with("node.")` is BANNED. List exact methods per role. +- **Session cookies: `SameSite=Lax; HttpOnly; Path=/`** — `Strict` breaks iframe app fetches. `Lax` still prevents CSRF on POST. +- **Destructive operations require password re-verification** — factory reset, onboarding reset, identity export. +- **Remember-me secrets: use `OsRng` random bytes** — never derive from `/etc/machine-id` or other public data. +- **Rotate session tokens after privilege escalation** — TOTP verification must issue a new token, invalidating the pending one. +- **Tar archive extraction: validate every entry path** — never use `archive.unpack()`. Iterate entries and verify no `..` components or paths escaping the target directory. + +### Frontend (Vue/TypeScript) + +- **Validate redirect URLs** — use `isLocalRedirect()` from `router/index.ts` before any `window.location.href` assignment. Reject `javascript:`, protocol-relative (`//`), and external URLs. +- **Never use `v-html` with user input** — if unavoidable, always sanitize with `DOMPurify.sanitize()`. +- **CSP: no `unsafe-inline` in `script-src`** — Vite builds don't need it. Keep `unsafe-inline` only in `style-src` for Tailwind. + +### Nginx + +- **Session validation: `$cookie_session` (not `$cookie_session_id`)** — cookie name must match the Rust backend's `session=` cookie. +- **Prefer `auth_request` over cookie-presence checks** — `if ($cookie_session = "")` only checks presence, not validity. For sensitive endpoints, use nginx `auth_request` to validate against the backend. +- **All `/app/*` proxies are unauthenticated at nginx level** — each app must handle its own auth. Never expose apps with default credentials (change Grafana `admin/admin` on first boot, etc.). + +### SSRF Prevention + +- **Validate all user-supplied URLs** — require `https://` scheme, reject private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7). +- **Disable redirect following** — use `redirect(Policy::none())` on reqwest clients that fetch user-supplied URLs. +- **Onion addresses: validate v3 format** — exactly 56 base32 `[a-z2-7]` chars + `.onion`. +- **Webhook URLs: parse with `Url::parse`** — don't split on `:` for host extraction (breaks IPv6). + +### Container Security + +- **Memory limits on every container** — use `--memory=$(mem_limit )` pattern from `first-boot-containers.sh`. Prevents one container from OOM-killing the system. +- **Health checks on every container** — define via `--health-cmd` in `podman run`. +- **User-stopped tracking** — when a user stops a container via UI, record in `user-stopped.json` so crash recovery and health monitor don't auto-restart it. + ## Code Quality - Zero compiler warnings (Rust and TypeScript)