homeio
PublicForked from doctor-io/homeio
security.md
Threat Model
Homeio is designed for a trusted LAN or a protected reverse-proxy deployment. It manages high-privilege host surfaces: Docker, files under FILES_ROOT, D-Bus network/power/storage actions, and terminal access. It is not currently hardened as an internet-facing multi-user service.
Defended:
- Session cookie tampering through HMAC-SHA256 signatures.
- Password disclosure through scrypt password hashing.
- File traversal outside
FILES_ROOTthroughresolvePathWithinFilesRoot(). - Large upload traversal in the Go upload sidecar through its own
FILES_ROOTpath jail and session-cookie HMAC validation. - PTY environment leakage through a terminal env allowlist.
Not fully defended:
- Brute force attempts across multiple app processes; the current limiter is in-memory.
- Middleware-level authorization for all
/api/v1/**routes. - Exposure of Docker socket capabilities.
Auth Mechanism
lib/server/modules/auth/session-token.ts signs {sessionId}.{expiresAtEpochSeconds} with HMAC-SHA256 using AUTH_SESSION_SECRET. The token is stored in the homeio_session cookie from lib/server/modules/auth/cookies.ts with httpOnly and sameSite: lax; the Secure flag depends on HTTPS request context.
Passwords are hashed in lib/server/modules/auth/password.ts using Node crypto.scrypt with a random salt. scrypt is a valid KDF, but argon2 is more common for new web password storage. The custom session format is simpler than JWT/PASETO and avoids token claims, but it is non-standard and relies on a strong AUTH_SESSION_SECRET.
app/api/auth/login/route.ts uses the in-memory limiter in lib/server/modules/auth/rate-limit.ts. Failed attempts are keyed by normalized username plus client IP from x-forwarded-for or x-real-ip; the current limit is 5 failures per 15 minutes. A successful login clears the key. Limited requests return 429 with { "error": "Too many login attempts" }.
lib/server/env.ts rejects unsafe production AUTH_SESSION_SECRET values at runtime. In production, known defaults (dev-session-secret-change-me, change-me-to-a-random-32-char-secret) and secrets shorter than 32 characters fail startup, except during the Next production build phase where runtime secrets may not be present.
Path Jailing
lib/server/modules/files/path-resolver.ts exports resolvePathWithinFilesRoot(). File operations should pass all user-provided paths through this resolver before touching the filesystem. The file manager service maps FilesPathError to FileServiceError.
Docker Socket Risk
docker-compose.yml mounts /var/run/docker.sock. Access to the Docker socket is effectively host-root equivalent in many deployments. Any route or bug that can trigger arbitrary Docker commands is high severity.
D-Bus Privilege Model
Network and USB features use services/dbus-helper/ over DBUS_HELPER_SOCKET_PATH. The sidecar talks to NetworkManager and udisks2 through D-Bus. It is a separate privilege boundary and must be tested separately when changed.
Upload Sidecar
Bare-metal installs build services/upload-server/main.go and run it through systemd on UPLOAD_SERVER_ADDR, defaulting to /run/home-server/upload.sock. The sidecar validates the homeio_session cookie using AUTH_SESSION_SECRET, rejects unsafe filenames and paths, writes only under FILES_ROOT, and exposes a simple /health endpoint. The socket is chmod 0666 so the app process can reach it; do not expose a TCP UPLOAD_SERVER_ADDR outside a trusted local boundary.
Google OAuth Secrets
Settings → Integrations stores Google OAuth client credentials in the singleton settings row. The client secret is encrypted with the existing file-secret encryption helper; the public settings API returns only client ID, redirect URI, and hasSecret. Connected Google Drive account access and refresh tokens are stored encrypted in files_google_drive_tokens.
Known Gaps
| Gap | Severity | Exploit scenario | Status |
|---|---|---|---|
| Missing middleware-level auth | Medium | A future route could bypass auth if it is outside the architecture test scope | Mitigated by requireApiSession() and architecture test |
| Login rate limiting is in-memory | Medium | Distributed deployments would not share brute-force counters across processes | Mitigated for single-process installs |
Unsafe production AUTH_SESSION_SECRET | High | Users who do not change it risk forged session tokens | Mitigated at runtime by lib/server/env.ts; development/test defaults still work |
| Docker socket mounted | High | App compromise can become host compromise through Docker | Accepted deployment trade-off |
| WebSocket terminal is full shell | High | Authenticated terminal access is host shell access, not command allowlist | Open documentation gap |
| Upload sidecar socket is broad-permission | Medium | Local host users could try authenticated upload requests through the socket | Mitigated by session validation and path jail; keep socket local |
| Disk manager destructive routes | High | Authenticated user can format, partition, or wipe host disks | Mitigated by single-user trust model and explicit UI confirmations |
passwordHash returned from session lookup | Low | Hash circulates in memory unnecessarily | Open |
services/dbus-helper/ plain JS | Medium | Lower type safety for privileged sidecar protocol | Open |
Mitigation Priorities
- Move from per-route helper auth to middleware or generated route wrappers if feasible.
- Consider persistent/distributed login rate limiting if Homeio ever runs multiple app processes.
- Separate terminal UX/docs for allowlisted command execution vs full PTY.
- Reduce Docker socket exposure where deployment topology allows it.