Ovumcy v0.9.5
Release date: 2026-05-15
Highlights
- Five-sprint security hardening pass driven by a methodology audit of every auth- and session-relevant surface in the codebase.
- Two Medium-severity findings closed end-to-end (#1 OIDC
end_session_endpointhost-pinning, #3 register-pickup cookie single-use), three Low-severity findings closed (#2 TOTP secret AAD binding, #5 session-version bump on 2FA toggle, #6 OIDC signing-algorithm allowlist), and three Info-level findings closed (#7 session-version bump on clear-data, #9 safe-by-construction HTMX error swap, runtime PoC suite for #1/#6). - New JavaScript unit-test suite, CLI subprocess smoke test, and OIDC runtime PoC suite — every new contract has a regression test.
Security
- OIDC
end_session_endpointis now host-pinned to the configured issuer. Discovery metadata that advertises an end-session endpoint on a different host is rejected at provider load time and the logout flow falls back to local-only logout. Closes a defense-in-depth gap where a compromised or look-alike discovery document could redirect the logout flow (including anyid_token_hintcarried in the URL) to an attacker-controlled host. - OIDC ID-token verifier carries an explicit signing-algorithm allowlist (
RS256,RS384,RS512,ES256,ES384,ES512,PS256,PS384,PS512,EdDSA). Symmetric algorithms andnonecannot be negotiated even if the upstream provider advertises them, closing an algorithm-confusion downgrade lane. - Register-pickup cookie is now single-use server-side.
POST /api/auth/registerpersists an opaque nonce in the newregister_pickup_tokenstable (migration 022);GET /register/welcomeatomically consumes the nonce in the same UPDATE. A captured sealedovumcy_register_pickupcookie can no longer be replayed within its 5-minute TTL to mint a second auth session — the second consume falls through to the same neutral/loginredirect as a decoy or expired pickup. - Encrypted TOTP secrets are now bound to the owner's user id via AES-GCM additional-authenticated-data (
ovumcy.field.totp_secret:<userID>). A database-level swap of one user's encrypted secret into another user's row no longer opens under the second user's id, so an attacker with database write privilege cannot pass 2FA for another account by lifting the ciphertext. A legacy fallback opens pre-aad ciphertexts written by older versions and lazily re-encrypts them under the new format on the next successful 2FA login, without bumpingauth_session_version. - 2FA enable and disable now bump
users.auth_session_versionatomically with the TOTP-field update. Every other auth cookie issued before the toggle is invalidated on its next request; the originating session is refreshed inline so the user that performed the toggle stays signed in on their current device. Matches the existing contract for password change, password reset, and recovery-code regeneration. POST /api/settings/clear-datanow bumpsusers.auth_session_versionatomically with the data wipe. A "panic clear" gesture from one device now invalidates every other session, and a stolen session that triggered the wipe loses its authenticated access on the next request.- HTMX error responses no longer flow through
target.innerHTMLon the client. The status-error fragment returned by the server is parsed withDOMParser, the message text is extracted viatextContent, and a fresh<div class="status-error">is built viadocument.createElement+replaceChildren. Server-rendered error templates already escape user-supplied values, so this is purely defense-in-depth — but any future regression that lets unescaped HTML into an error response is now safe by construction rather than an instant DOM-XSS sink.
Added
- Frontend JavaScript unit-test suite under
web/src/js/__tests__/, run vianpm run test:unit(Node's built-in--testrunner backed by jsdom). Twenty-seven tests pin the four security-sensitive client-side surfaces previously exercised only indirectly through Playwright: CSRF token injection onhtmx:configRequest, the safe-by-construction DOM swap onhtmx:responseError, theisSafeClientTimezonevalidator backing theovumcy_tzcookie write, and thenavigator.clipboard→document.execCommand("copy")fallback used by the recovery-code copy UI. Wired into the CI workflow alongsidelint:js. - Subprocess smoke test for the operator CLI (
go test ./cmd/ovumcy -run TestCLISubprocessSmoke). Builds the realovumcybinary into a temp directory and runs three subprocess invocations to cover argv parsing, env-var pickup, exit codes, and the SECRET_KEY-validator independence ofusers/reset-passwordsubcommands. Skipped undergo test -short. - Runtime PoC regression tests for the OIDC hardening (
internal/security/oidc_runtime_poc_test.go). Stands up a controlled OIDC provider over real TLS with a per-test CA and exercises the productionsecurity.OIDCClient.loadProvider+*oidc.IDTokenVerifieragainst malicious discovery metadata and a forged algorithm-confusion token. Five contracts pinned: cross-originend_session_endpointrejected, same-origin accepted, HS256-with-RSA-public-key-as-HMAC-secret rejected,alg=nonerejected, mock-provider sanity smoke.
Upgrade notes
- Schema migration: running v0.9.5 for the first time will apply migration
022_register_pickup_tokens.sql(SQLite and PostgreSQL). The migration is additive — no downtime expected. The new table is bounded by the register rate limit and a 5-minute row TTL; rows are cleaned up byDeleteExpired(see SECURITY.md for the contract). - Downgrade caveats: as documented in docs/self-hosted.md → Downgrade Caveats, the two migrations that warrant operator attention are 019 (date canonicalization) and 022 (register-pickup tokens). Both downgrade scenarios produce graceful UX degradation rather than auth bypass, but the operator should keep a pre-019 / pre-022 backup if they intend to roll back through either boundary.
- Legacy TOTP secrets: existing TOTP-enrolled accounts whose secret was encrypted by a pre-v0.9.5 binary continue to work. The new code opens them through a legacy fallback path and lazily re-encrypts them under the aad-bound format on the next successful 2FA login. No operator action is required.
- OIDC operators: the new signing-algorithm allowlist is RS/ES/PS + EdDSA. If your IdP only signs with HS256, sign-in will fail closed. Reconfigure the IdP to use RS256 or one of the other allowed asymmetric algorithms.
- Tagged images publish under
ghcr.io/ovumcy/ovumcy-web:v0.9.5. - Existing deployments can upgrade in place by pulling the new image or by setting
OVUMCY_IMAGE=ghcr.io/ovumcy/ovumcy-web:v0.9.5.
Full changelog
- Compare: v0.9.4...v0.9.5
- Changelog entry:
CHANGELOG.mdsection0.9.5.