security: harden web server, daemon socket, and write paths

Security review findings, fixed and verified:

Critical
- Fix unauthenticated arbitrary file read: the SPA catch-all route
  joined the raw URL path onto the dist dir without containment, so
  encoded '..' segments (/%2e%2e/etc/passwd) leaked any file readable
  by the root server. Resolve with realpath and reject paths outside
  the dist dir (fail-closed 404).

High
- Daemon socket: serve_start no longer accepts caller-chosen
  host/port. The socket is world-connectable (unprivileged CLI users),
  so callers could previously rebind the root web server to 0.0.0.0.
  The daemon now always binds the operator-configured address and
  reports it in the response; the CLI warns on mismatch.

Medium
- Remove the per-request max_delta_khz override from the API: the
  server-enforced safety cap is now authoritative. CLI direct paths
  (write, profile apply, verify) honor the configured cap; --max-delta
  still overrides for explicit root use.
- Snapshot restore: confine filepath to the snapshot directory
  (realpath containment; blocks symlink escapes).
- Login lockout: honor X-Forwarded-For only for peers listed in the
  new trusted_proxies config (rightmost untrusted hop), so the
  per-IP lockout works behind a reverse proxy. Spoofed headers from
  untrusted peers are ignored.
- /api/shutdown: new allow_api_shutdown config (default true);
  shared systems can disable the API shutdown path.

TLS (opt-in, like auth)
- New ssl_certfile/ssl_keyfile config + CLI flags (serve start,
  service install/configure, --no-ssl to disable). When active:
  HTTPS for UI/API, wss:// for WebSockets, Secure session cookie,
  CLI auto-switches to https://. Cert/key paths are validated up
  front with a clear error instead of a silent uvicorn crash.

Tests & docs
- tests/test_security.py: standalone regression tests (no new deps)
  covering SPA containment, snapshot containment, cap removal,
  client-IP derivation, proxy normalization, TLS scheme detection,
  and daemon host/port hardening.
- README + Usage-Guide: TLS section, new config keys, updated
  security notes.
This commit is contained in:
ARIA committed 2026-09-10 16:19:21 +02:00
1 parent 8956fc9d7b
commit 39701c12ff
9 files changed
+648 -55

No files matched your search

+9
View File
@@ -121,6 +121,15 @@ def restore(gpu, snapshot_dir: str, filepath: str | None = None) -> bool:
print(f"Snapshot file not found: {filepath}")
return False
# Contain the path inside the snapshot directory — callers (in
# particular the HTTP API) must not be able to point the restore at
# arbitrary files on the filesystem.
snap_dir = os.path.realpath(snapshot_dir)
resolved = os.path.realpath(filepath)
if not resolved.startswith(snap_dir + os.sep):
print(f"Snapshot path outside snapshot directory: {filepath}")
return False
try:
with open(filepath, "rb") as f:
raw = f.read()