Security model
Threat model, hardening defaults, and the audit trail of fixes that shaped the current posture.
Threat model
The plugin + service combo sits inside your home LAN by default. We assume:
- Trusted: the Jellyfin server itself, the host OS, the Docker daemon.
- Semi-trusted: authenticated non-admin Jellyfin users. They can play media but should not be able to read server paths, manipulate the service, or enumerate other users' jobs.
- Untrusted: everything outside the LAN. If you expose Jellyfin via reverse proxy, assume the attacker can hit every plugin endpoint with a valid user token.
Authentication
Plugin controller (/Upscaler/*)
- Standard Jellyfin auth — every endpoint requires a valid
X-Emby-Token. - Mutating / privileged endpoints additionally require the admin role via
[Authorize(Policy = "RequiresElevation")]: model download, model load/unload, wrapper install, active-jobs list. - Read-only endpoints used by the in-player menu (libraries, models, recommend-model) allow any authenticated user.
AI service (:5000)
- Shared-secret token in
X-Api-Token, configured via theAPI_TOKENenvironment variable. - If
API_TOKENis unset, the service runs with no auth — fine inside a closed LAN, unacceptable if the port is reachable from the internet. /healthis intentionally public (for container orchestrator probes).
Never expose
:5000 directly to the internet even with a token. Put it behind a reverse proxy that enforces the token at the edge and rate-limits — see Deployment → Reverse proxy.
Input validation
- Model IDs + preset IDs: validated against
^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$on both sides. Traversal patterns (.., leading or trailing dots) are rejected before any filesystem access. - Item IDs: parsed as Jellyfin
Guid. Malformed input returns 400 before hitting the library. - Base64 frames: size-limited server-side; oversized payloads return 413.
- Filter sliders: clamped server-side to their documented ranges; the client's clamping is not trusted.
Audit history
Notable security-relevant fixes from the changelog:
| Version | Finding | Fix |
|---|---|---|
v1.6.1.11 |
/Upscaler/jobs leaked absolute file paths to any authenticated user, not just admins. |
Endpoint gated with [Authorize(Policy = "RequiresElevation")]. Path strings additionally filtered to show relative segments only. |
v1.6.1.9 |
Model ID regex was permissive, allowed dot-segments without constraints — theoretical path escape. | Tightened regex to explicit dot-separated segments: ^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$. |
v1.6.1.7 |
Wrapper-install endpoint accepted arbitrary command strings from the request body. | Rewritten to generate a fixed template on the server; request body is ignored. |
v1.6.1.4 |
Service-side /logs-stream SSE exposed full log content without auth when the shared token wasn't set. |
Moved behind the same token as every other endpoint; health probe remains the only anonymous route. |
Hardening checklist (public / semi-public deploys)
- Set
API_TOKENto a long random value. Rotate on any suspected leak. - Do not port-forward
:5000. Put it behind a reverse proxy and enforce the token at the proxy as well. - Disable FFmpeg Remote Transcoding if you don't use it; the SSH path is a potential sharp edge.
- Run the AI service container with
read_only: trueexcept for/app/models. - Pin an explicit image tag (
kuscheltier/jellyfin-ai-upscaler:v1.6.1.16-cuda), notlatest-cuda. - Keep Jellyfin on 10.11.x — older LTS lines don't receive the auth-policy fixes the plugin depends on.
- Review the weekly container digest against the published SHA before updating.
Reporting vulnerabilities
For a security issue (not a functionality bug), do not open a public GitHub issue. Instead:
- Use GitHub's Private vulnerability reporting on the repository Security tab.
- Alternatively, email the maintainer as listed on the GitHub profile with
[security]in the subject. - Expect acknowledgement within 72 hours and a patch timeline within 10 days for reproducible, in-scope issues.
Out of scope
- Jellyfin core vulnerabilities — report to jellyfin/jellyfin.
- Docker / container-runtime escapes — upstream Docker/Moby issue.
- Social engineering, physical access, or any scenario requiring prior root on the host.
- Findings in deliberately-public info (weights on HuggingFace mirrors, public schema).