Recovery email displayed as verified before verification was completed
Summary
A defect in the meil.no signup flow caused a secondary recovery email to be displayed as verified immediately after signup, without a verification mail being sent. The defect was reported by a customer, root-caused, and fully fixed within nine hours, with backfill verification mails sent to all 106 affected legacy users on the same day.
What Happened
meil.no allows a user to configure a recovery channel — either a phone number (SMS) or an email address — during signup. The OTP-verified primary channel is the one the user actively confirms during the signup flow. A user may also provide a secondary recovery address (typically an email) that does not need to be verified at signup time.
During an internal review prompted by a customer report, we discovered that the verification status for the primary and secondary channels was stored in a single shared database column. The signup routine stamped this column to the current timestamp at user creation. The user interface in Settings then displayed both channels as verified, including the secondary one for which no verification mail had ever been sent.
The defect was structural rather than a one-off bug: it affected every user who configured a secondary recovery address at signup, regardless of platform region or signup channel.
Impact
Two effects were possible.
The primary effect was a user-interface integrity issue: 107 accounts were affected, of which 106 required backfill verification emails. The remaining account was an internal test account verified manually during fix-validation. Affected users were shown a verified badge on a recovery channel that the platform had not in fact verified. This could give those users a false sense of recovery redundancy.
The secondary effect, identified during root-cause analysis, was that the platform's password-reset flow listed the unverified secondary address as a valid destination for one-time codes. The flow allows a user to choose exactly one destination — it is not a two-factor flow — and several layers of defense remained in place: the destination is shown only as a partial mask, the user must enter the full address to confirm before any code is sent, exponential backoff locks an account after repeated mismatched attempts, and per-IP and per-email rate limits apply to the flow. These defenses meant that exploitation would require an attacker to already know both the victim's login email and the full unverified secondary address — a narrow scenario rather than a broad enumeration risk.
We have found no evidence that any password-reset code was sent to an unverified destination, and no evidence of any account compromise resulting from this defect.
Actions Taken
- The recovery verification status was migrated from a single shared column to two per-channel columns, so a verified phone and a verified email each carry their own independent timestamp.
- The signup routine was updated to stamp the verification timestamp only on the channel the user actually verified during signup. Secondary recovery emails now receive a one-time magic-link mail with a seven-day expiry, generated and stored at signup time.
- A new public landing page at /verify-recovery-email handles six outcome states (verifying, success, already verified, expired, invalid, conflict) in Norwegian, English, Swedish, and Danish.
- The Settings → Recovery view in the user interface now shows an explicit "Not yet verified — verify now" call-to-action for any recovery channel that has not been verified, with a resend option that rotates the token.
- The password-reset and change-password flows were updated to filter on per-channel verification status. An unverified address is no longer listed as a valid destination and is rejected if supplied.
- A backfill mail was sent to all 106 affected legacy users between 07:44 and 07:46 UTC on 2026-05-21, using the same magic-link flow. Approximately 9 verifications were completed within the first 90 seconds.
- A regression test was added to ensure any future change that re-introduces a shared verification timestamp fails the build.
Preventive Measures
- Going forward, any state that represents a verification outcome on a multi-channel record will be modeled per-channel from the outset, with the verification path required to write the corresponding field. Implicit "verified at row creation" semantics will be treated as a defect.
- The password-reset and change-password paths will explicitly verify that any destination they accept has independently completed a verification step, regardless of how the destination was originally added.
- Customer reports remain a first-class signal in our defect-discovery process. This defect was identified end-to-end by a customer report. We continue to invest in making it easy and welcomed for customers to flag anything that looks wrong.
Affected Services
Acknowledgement
- Kathrine Jølle Wathne — for responsibly reporting this issue and helping improve the security, reliability and integrity of meil.no
