Skip to content

v2.5.22

Latest

Choose a tag to compare

@github-actions github-actions released this 20 Aug 18:22
· 2 commits to main since this release

v2.5.22

This release closes a security-control bypass: "Disable password sign-in" did not actually hold. If you rely on that switch, update. It also adds OIDC sign-out at the identity provider, makes the security score credit a passwordless setup, and stops a confusing 500 on the login page. Drop-in from any 2.5.x — no config or data migration.

Security — "Disable password sign-in" now actually disables password sign-in

Who is affected: servers with Disable password sign-in turned on. If you have never enabled it, nothing here changes your exposure. Present since the feature shipped in v2.5.11.

What this is: a security-control bypass, not an authentication bypass. An attacker still needed valid credentials — what failed was the mitigation the switch exists to provide, namely that a leaked or weak password stops being sufficient. An admin who turned it on was told nothing about the gaps.

Two independent ways password sign-in still succeeded with the switch on and every escape hatch off:

  • Only one of Jellyfin's two password endpoints was gated. The plugin matched POST /Users/AuthenticateByName, but Jellyfin also still routes the deprecated POST /Users/{userId}/Authenticate?pw=…. It carries no authorization requirement, is hidden from the API documentation, and reaches the same authentication code — so nothing keyed to the HTTP path ever ran for it. The user id it needs is not a secret: GET /Users/Public hands out ids and names to anyone.

    That endpoint skipped three controls, not one. Along with the password-login switch, it also bypassed empty-password blocking and per-account lockout — including the failed-attempt counter, which for accounts on Jellyfin's default password provider has no other hook. In practice that made it an unthrottled, unaudited password-guessing path.

  • The SSO waiver was a string test, not a lookup. The gate let a request through when the submitted password merely started with the internal oidcbr_ prefix used by the OIDC bridge. Any user could opt themselves out of the server-wide policy by choosing a password beginning with those characters, and nothing in the UI or the logs showed they had. This one only affected accounts on Jellyfin's default password provider — SSO and passkey accounts were already rejected by the plugin's own authentication provider.

Both are fixed. The gate now matches both password endpoints (reading the password from the query string and the account from the route id where required), and the SSO waiver is a real lookup against the live token store rather than a prefix comparison. Quick Connect is deliberately still exempt, and the admin / LAN / CIDR escape hatches behave exactly as before on both endpoints.

Reported privately by @camarigor, with a reproduction and a suggested fix. Found while analysing #160 — the observation being that the security score could not honestly credit the switch while these held. Verified fixed against a live server by replaying both bypasses on the previous build and then on this one.

Related hardening: the trust-cookie middleware had the same blind spot on the deprecated endpoint. That one failed closed — the effect was simply that no trust cookie was issued — so it was never exploitable, but it is fixed alongside so both predicates cover the same endpoints.

Sign out of your identity provider, not just Jellyfin

(Closes #170, raised by @Akruidenberg in #134. Implemented by @camarigor.)

Signing out of Jellyfin previously ended only the Jellyfin session, leaving the identity provider session live — so "Sign in with …" walked straight back in with no prompt, and the user had not really signed out of anything.

Enable RP-initiated logout per provider (off by default) and signing out now hands the browser to the provider's own sign-out endpoint afterwards. Optionally set a return URL to come back to. Every failure path lands on the normal Jellyfin login page instead of an error, because someone who clicked Sign out should never end up looking at a stack trace.

Note: this needs the provider to publish an end_session_endpoint in its discovery document. Keycloak and Authentik do. Google does not — with a Google provider the toggle has no effect and sign-out behaves exactly as it did before. That is the intended graceful degradation, not a bug.

Turning off password sign-in now improves your security score

(Closes #160, requested by @hax4dazy.)

A server that has switched password sign-in off is genuinely more resistant to password compromise, and the score now says so.

The credit is additive — the factor only appears when the switch is on. Running passwords with 2FA is a perfectly sound posture, and a factor those servers could never earn would have quietly dropped their grade on upgrade. It is also graded: leaving the administrator, LAN, or exempt-network escape hatches open scores partial credit, because password compromise is still in the threat model for those clients.

Fixes

  • A user at their session limit no longer gets "Internal server error". (Fixes #178, reported and fixed by @camarigor.) With the correct password and a correct 2FA code, a user who had hit Maximum number of simultaneous sessions saw a generic internal error pointing them at the [2FA] logs — where the 2FA step had, confusingly, already succeeded. The real reason now surfaces directly, and this refusal no longer counts toward the per-IP ban counter, so hitting your own session cap can no longer get your own address banned.

  • Jellyfin 12 readiness: authenticated requests use the current authorization header. (#174 by @martin-77, completed in #180.) The plugin's own pages moved from the legacy X-Emby-Token header to Authorization: MediaBrowser Token="…", which is required once an operator disables legacy authorization on Jellyfin 12. Three server endpoints still read only the legacy header, which would have broken the onboarding sign-out button, silently weakened the QR device-pairing ownership check, and stopped the session list marking your current device — all three now accept either form. The legacy header still takes precedence when present, so nothing changes for clients that send it.

    This does not yet make the plugin load on Jellyfin 12; that needs a separate build and is tracked in #172.

  • The startup empty-password audit warning now names both password endpoints rather than only /Users/AuthenticateByName.

Maintenance

  • CI action pins bumped with the CodeQL family moved in lockstep, and a Dependabot group added so those three entry points can no longer deadlock each other. (#164, #166 by @camarigor.)
  • Dependabot pull requests that touch a src-side package now get every dependent lock file regenerated automatically, and a locked-mode restore failure explains which files to regenerate. (#166.)
  • Dependency bumps with all lock files regenerated. (#165, #175.)
  • SECURITY.md now tells a reporter what to do when both private channels are unavailable, and private vulnerability reporting is now enabled on the repository, so the link in that document works. (#169, #168 by @camarigor.)

Upgrade

Drop-in from any 2.5.x. No configuration or data migration. Existing 2FA enrolments, passkeys, trusted devices, and app passwords are unaffected.

If you use Disable password sign-in, this is the release that makes it hold — worth updating promptly.