jellyfin-tools/jellyfin-year-review/docs/security.md
jellyfin-year-review/docs/security.mdRaw
1# Security and operational boundaries
2
3**Online authentication (default):** the operator configures one Jellyfin API URL and one exact public app origin. The login request cannot choose the API target. Credentials go only server-to-server to `/Users/AuthenticateByName`; passwords are not persisted, logged or returned. They live briefly in request/browser input memory. Jellyfin replies and network errors are never echoed into logs. No request logging middleware, analytics, third-party scripts or error-reporting SDKs are included.
4
5Each login generates a random, separate app device identity. The authentication result's user object is not trusted for authorization: the returned token is immediately checked with `/Users/Me`. Every private API request checks it again and compares the current ID to the stored identity. Disabled users and deleted/revoked identities lose access. During upstream failure access fails closed with 503. A Jellyfin admin receives their own review, with no cross-account switch.
6
7App session IDs have 256 bits of cryptographic entropy; only SHA-256 keys are kept in a bounded in-memory map with server-only Jellyfin tokens (online) or per-account credential revisions (offline), and absolute 12-hour expiry. No signed/encrypted client payload is needed. Cookies are HTTP-only, SameSite Strict, path `/`, with Secure for configured HTTPS. Forged/modified IDs are absent from the store; there is no client identity claim to forge. Restart invalidates all app sessions. Maximum 1,000 active sessions; expired entries are cleaned opportunistically.
8
9In online mode, logout removes the app session/cookie before calling Jellyfin `/Sessions/Logout` with only that device token. A Jellyfin failure still completes local logout and tells the user to revoke the Reel Review device manually later. There is no retry queue and no persisted token recovery. App expiry/restart discards tokens but does not promise upstream device cleanup. Operators may remove stale Reel Review devices from Jellyfin. Other clients' tokens are never touched.
10
11CSRF defense requires the exact configured Origin on every POST, rejects cross-site fetch metadata, accepts JSON only, and uses SameSite Strict cookies. Forwarded headers never override origin or the peer-IP login limit. JSON request bodies are limited to 4 KiB; headers to 16 KiB. Server/client deadlines bound reads, writes and upstream calls. Login allows 10 attempts per socket peer per minute and 60 globally, with a maximum 4,096 rate buckets; full capacity fails closed. This limit is in memory and resets on restart. Put the service behind an HTTPS reverse proxy on a private network and use its connection/rate controls for public exposure.
12
13Review IDs come exclusively from validated sessions; a normalized year selects a manifest-listed document. Extra user selectors are rejected. Generated JSON and manifests are outside the public webroot. Paths and artwork filenames are allowlisted. Static frontend reads use Go's rooted filesystem and reject directories, traversal and unknown paths. Dataset directories must remain operator-controlled; never give untrusted users filesystem write access to them.
14
15The production CSP uses same-origin scripts plus hashes of SvelteKit's compiled inline bootstrap; no `unsafe-inline` script permission or eval. Styles permit inline values for charts; images, fonts and fetches use self only. Objects, frames, base URLs, camera, microphone and geolocation are blocked. HSTS is emitted on configured HTTPS. Svelte escapes all displayed text and the application never uses unsanitized HTML insertion. Artwork is fetched during generation from the configured Jellyfin server, with TVmaze as a fallback for series. Jellyfin redirects are not followed and its operator API key is sent only in request headers to that server. TVmaze uses a separate credential-free client restricted to HTTPS at `api.tvmaze.com` and `static.tvmaze.com`; redirects may remain only on the same host. No user identities, passwords, watch dates or viewing durations are sent to TVmaze; searches use series titles, premiere years and TVDB/IMDb identifiers. Image response sizes and dimensions are bounded, and complete JPEGs are validated before caching/publication. The frontend has no third-party artwork or metadata requests.
16
17The Docker context uses an allowlist. Private environment files, source exports, generated data, caches, Git metadata, tests, and local builds are excluded. The runtime is non-root, has no shell, and the Compose service has a read-only root and data mount, dropped capabilities and no-new-privileges. Generator mounts are explicitly separate. A private filesystem backup contains sensitive viewing history, even if users later disappear from the active manifest. Apply your own retention policy; the application does not automatically erase archival datasets.
18
19Resource scope is a small trusted operator deployment. Source JSON snapshots are bounded at 256 MiB, raw rows at 1 MiB, inventory at one million records, metadata responses/JPEGs at 5 MiB and image dimensions at 4096². Processing time grows with history length and exact interval minutes. Generator memory/CPU limits in Compose constrain accidental overload. No live deployment, database mutation, or external CI service is part of this project.
20
21## Offline authentication
22
23Offline mode constructs no Jellyfin client. The separately mounted private `auth/users.json` is its authority for logins and account validity; it is reread on every private request. Salted Jellyfin 12.2 PBKDF2 verifiers never enter generated review JSON or the public webroot. Supported current/legacy algorithms use Go's standard-library PBKDF2 and constant-time comparison with bounded parameters and two concurrent derivations. Unknown/disabled accounts perform dummy default-cost derivation and receive the same generic authentication failure. Group/world-readable files, symlinks, unsupported schemas and unsafe credential placement are rejected. Invalid exports fail closed; there is no fallback to live Jellyfin or a stale credential cache.
24
25Fingerprint changes revoke affected sessions after credential updates. Logout is local-only. No disconnected service can learn of changes made in Jellyfin without a new export; old passwords and stale account status remain authoritative until refresh. Python's source exporter selects Users and Permissions within one read-only SQLite transaction, including WAL contents, and saves a private `.previous` backup before atomic replacement. It excludes external/passwordless accounts and blocks disabled or restricted accounts. Protect both the export and its backup against theft and offline guessing of Jellyfin passwords. Only transfer them privately over SSH. The standalone offline Compose file needs no media network or upstream credentials. Full procedures and supported policies are in [offline-auth.md](offline-auth.md).