Ovumcy v1.1.0
Release date: 2026-05-23
Highlights
- Security: TOTP step-up requirement on
POST /auth/oidc/link-confirmcloses a 2FA bypass where an attacker with the victim's password plus a malicious / sloppy upstream IdP could obtain a session for a TOTP-protected account without holding the second factor. The link-confirm form now requires a valid 6-digit code together with the password whenever the target account has TOTP enabled. - Bug: issue #64 — UTC-minus locales (e.g. America/Toronto, America/New_York) hit
duplicated key not allowedon every second day-save, surfaced to the user as "failed to create day" / "auto-save failed". Root cause was double-canonicalization of the lookup window inUpsertDayEntry; consecutive day upserts now correctly update the canonical row. - Coverage: backend regression suite grows by 41 tests across security-sensitive surfaces (OIDC link-confirm flow, AEAD cookie codec for
ovumcy_oidc_link_pending, DeleteDay handler, cross-user privacy boundaries, rate-limit responders, basic identity handlers, ShowResetPasswordPage).internal/apiline coverage moves from 70.8% to 75.4%.
Security
- TOTP step-up on
/auth/oidc/link-confirm. When the target local-auth account has TOTP enabled, the link-confirmation submission must additionally carry a valid 6-digittotp_codeform field. The handler runsTOTPService.CheckRateLimitandValidateCodewith the same per-(client_ip, user_id)failure counter and replay rejection (ErrTOTPReplayed) asPOST /api/v1/sessions/2fa-challenge. The identity link and session cookie are only issued after both factors pass; on TOTP failure the pending-link cookie stays alive for retry within the 5-minute TTL, same as wrong-password. Previously the handler calledAuthService.AuthenticateCredentials(password only) and went straight tosetAuthCookie, allowing an attacker holding the victim's password plus a malicious or sloppy upstream IdP to obtain a session — and persist a linked OIDC identity — without ever holding the second factor. - AEAD codec coverage for
ovumcy_oidc_link_pending. The cookie landed in v1.0.0 without dedicated codec regressions. v1.1.0 adds the canonical four-invariant lock (round-trip + cross-purpose AAD + tampered-byte + rotated-key) plus payload expiry and builder field validation, in line with the AEAD-sealed cookie coverage rule. - CSRF route-level locks.
POST /auth/oidc/link-confirmandDELETE /api/v1/days/:datepreviously had handler-level coverage on a no-CSRF test app. v1.1.0 adds explicit regressions that run with the real CSRF middleware enabled and assert403when thecsrf_tokenform field is missing, closing the SECURITY.md route-coverage requirement for both endpoints. - Cross-user privacy regressions. Owner B's
GET /api/v1/symptomsmust not surface Owner A's custom symptoms; Owner B'sDELETE /api/v1/days/:datemust not remove Owner A's row on the same calendar day. Both invariants were enforced at the service layer throughuser_idscoping but had no explicit regression — v1.1.0 adds one. - Sensitive-field leak guard on
GET /api/v1/users/current. Adds an explicit deny-list assertion (password_hash,recovery_code_hash,totp_secret,$2a$,$2b$) so any accidental field-leak from a future refactor is caught immediately.
Fixed
days(issue #64):UpsertDayEntrywas re-applyingDayRangeto a value already canonicalized to UTC-midnight by the caller. For UTC-minus localesDateAtLocationshifted the lookup window one day backward, so a secondPUT /api/v1/days/{date}for the same calendar day missed the existing row and the follow-upCreatecollided with theuidx_user_dateunique index. The handler now uses[dayStart, dayStart+24h)bounds directly, plus a defensive UTC-midnight normalization at the function entry so a future caller passing a non-canonicaltime.Timecannot reintroduce the same shift. Locked by integration tests on SQLite and Postgres (America/Toronto UTC-5 and Asia/Tokyo UTC+9 locales).
Changed
- Link-confirm form (
/auth/oidc/link-confirm) conditionally surfaces a TOTP input whenTOTPRequired=true(computed inShowOIDCLinkConfirmPagefrom the target user'sTOTPEnabled). The new field reuses theauth.2fa.code_label/auth.2fa.code_placeholderi18n keys. SECURITY.mdgains a new### OIDC Account Linkingsection in the Test Enforcement Matrix that links every claim about the link-confirm flow (password gate, TOTP gate, AAD-bound cookie, CSRF,local_auth_enabled=falserefusal,MustChangePasswordrouting, audit-log emission) to the specific Go test that enforces it. The threat-model bullet on malicious-IdP account takeover is extended to mention the TOTP gate.DeleteDayhandler gains an idempotency lock:DELETE /api/v1/days/{date}on a day that has no row returns 204 (previously implicit behavior, now explicit regression).- Rate-limit responders test surface now covers the HTML fallback path on
RespondAPIRateLimitedin addition to the JSON envelope, and asserts theretry_after_secondsshape across auth-form / settings / global path routing.
Upgrade notes
- OIDC operators with 2FA users. If you have local-auth accounts with TOTP enabled that may receive first-time OIDC links from a freshly-configured IdP, those users will now be required to enter a 6-digit code together with their password on
/auth/oidc/link-confirm. The form surfaces the TOTP input automatically when the target account has TOTP enabled; no operator configuration change is needed. - UTC-minus deployments (anything west of UTC, e.g. Americas) —
PUT /api/v1/days/{date}consecutive saves on the same calendar day now succeed instead of returning500on the second save. Users who saw "failed to create day" / "auto-save failed" toasts will stop seeing them after upgrade. - No new schema migrations in this release. v1.0.0 shipped through migration
022_register_pickup_tokens.sql; v1.1.0 only changes handler behavior, template form fields, and tests. Migration history is identical between v1.0.0 and v1.1.0. - Tagged images publish under
ghcr.io/ovumcy/ovumcy-web:v1.1.0. - Existing deployments can upgrade in place by pulling the new image or by setting
OVUMCY_IMAGE=ghcr.io/ovumcy/ovumcy-web:v1.1.0.
Full changelog
- Compare: v1.0.0...v1.1.0
- Changelog entry:
CHANGELOG.mdsection1.1.0.