Security
Running this on the public internet is on you: put it behind HTTPS and whatever access control you would give any other self-hosted app. What follows is what the app does on its own.
What the server holds
Almost nothing. The app is a client for api.nuvio.tv, and your account, your
profiles, your addons, your library and your settings live there.
The exceptions:
- Two cookies.
nuvio_sessionholds the upstream session (access token, refresh token, expiry, user) andnuvio_profileholds the picked profile index. Both arehttpOnly,sameSite=lax, andsecureoutside dev, with a thirty-day lifetime. The session is refreshed transparently when it expires, and a refresh that fails clears the cookie rather than looping. - The admin database, covering sign-ins and the allowlist for this instance. See The admin page.
Subtitle conversion and cast-biography lookups both happen in the browser, so neither subtitle files nor third-party biography text transit the server.
Outbound requests to addons
Addon URLs are user-supplied and the server fetches them, so every addon fetch goes through an SSRF guard:
- Scheme allowlist —
https, plushttponly where an addon manifest needs it. - Address blocks — loopback, private, link-local, carrier-grade NAT, benchmark, multicast and reserved ranges, for both IPv4 and IPv6, including both spellings of an IPv4-mapped IPv6 address.
- Manual redirect following, so every hop is re-checked rather than only the first.
DNS rebinding between check and connect is still possible in principle. Put an egress filter in front if that is in your threat model.
Addon responses are treated as untrusted data throughout: an entry missing what installing it needs, or what displaying it needs, is dropped rather than rendered.
The download proxy
/api/downloads/proxy?url=… passes a download's bytes through when the browser
can't fetch the source itself (no CORS headers, or http: under an https:
app). It isn't an open proxy:
- it answers signed-in users only;
- the target goes through the same SSRF guard as addon URLs, with private, loopback and link-local addresses refused and every redirect hop re-checked;
- only
Rangegoes upstream, and only content headers come back (no cookies either way); - the body streams through and is never stored.
The download worker tries every source directly first, so the proxy only carries traffic that has no other way.
Response headers
Every response carries:
- a Content Security Policy with
default-src 'self',frame-ancestors 'none',object-src 'none',base-uri 'self'andform-action 'self'.img-srcandmedia-srcare open tohttps:because addon artwork and streams come from arbitrary hosts;frame-srcallows onlyyoutube-nocookie.com, for trailers. x-content-type-options: nosniffx-frame-options: DENYreferrer-policy: strict-origin-when-cross-originpermissions-policydenying camera, microphone, geolocation and browsing-topicsstrict-transport-securityoutside dev
script-src includes 'unsafe-inline'. That is unavoidable without SvelteKit's
CSP nonce integration, which this setup does not use; everything else is locked
down.
Errors
Uncaught errors get a short random error id, logged server-side with the stack and shown to the user as just the id. In production the message is replaced with a generic one, because an unknown error is the only kind whose text could carry internals — a driver string, a path, a query.
An error thrown deliberately by the app (a guard's 403, a rejected remote function argument) keeps the message that was written for a user to read, and is logged at warn rather than error: it is the app working as designed.
Authorization
Route guards read the request's own service scope. requireProfile() demands a
signed-in request with an active profile; requireAdmin() 404s anyone not in
NUVIO_ADMIN_EMAILS.
Guards run in loads and in remote functions independently. A remote function never assumes the page that called it was guarded.
Reporting something
Open an issue on the repository. If it is sensitive, say so in the issue without the details and a private channel will be arranged.
This guide lives in the project repo: edit it there, and this page follows within a day.