Voke Cyber Security Advisory

CVE-2026-16751: Account Takeover in Ente via Emergency-Contact Recovery Bypass

Louis Sanchez Published July 26, 2026 9 min read
CVECVE-2026-16751
AdvisoryGHSA-9jcx-2fcp-hxmf • CERT/CC VU#834187
SeverityCritical  CVSS v3.1 base score 9.6
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
WeaknessesCWE-639 — Authorization Bypass Through User-Controlled Key (IDOR) • CWE-863 — Incorrect Authorization
ProductEnte museum server (Ente Photos, Auth, and Locker)
Affectedapi.ente.io and self-hosted deployments, from December 2024 through the fix
Fixed inPatched  Vendor fix in PR #11311
Reported byLouis Sanchez — Voke Cyber
Vendor responseEnte confirmed and fixed the issue; coordinated disclosure via CERT/CC.

Fixed — update self-hosted deployments

Ente confirmed and patched this in PR #11311. Hosted users on api.ente.io are already covered. Anyone running a self-hosted Ente museum server should update to a build that includes PR #11311, and can audit recovery records for sessions that moved from waiting to ready faster than the configured notice period — the fingerprint of this bug being exercised.

Summary

Ente lets you nominate an emergency contact who can help recover your account if you are locked out. To keep that feature from being abused, a recovery started by an emergency contact does not take effect immediately — it sits in a waiting period (commonly 30 days, configurable up to 60) during which you are emailed and can reject it. The endpoint that marks a recovery as approved never checked that the recovery session actually belonged to the person calling it. A former emergency contact could start a recovery and then, in the very next request, flip it from waiting straight to ready — collapsing the multi-day safety window to zero — reset the victim's password, and let Ente's own recovery flow wipe the victim's two-factor. A full account takeover in under a minute, against the one feature whose entire purpose was to be safe.

Background: the one feature that is supposed to be safe

Ente is a privacy-first, end-to-end encrypted service for photos, authentication codes (Ente Auth), and passwords and secrets (Ente Locker). Its whole promise is that nobody — not even Ente — can reach your data without your keys.

Emergency contacts are the deliberate exception. You nominate someone you trust, and during that setup your recovery key is intentionally wrapped to their public key, so that if you are ever locked out they can help you recover. Because that is a genuine break-glass capability, Ente puts a delay in front of it: a recovery started by an emergency contact enters a waiting period during which you are notified and can reject it. The waiting period is the entire safety mechanism. It is what separates "a trusted contact can help me recover" from "a former trusted contact can silently seize my account."

Root cause: the approval step never checks ownership

Emergency recovery moves through a small state machine: a contact starts a recovery, it waits, then it becomes ready, then the password can be changed. The bug is in the step that moves a recovery from waiting to ready.

The approval handler checks that the caller claims to be approving as themselves. What it never does is load the recovery session named in the request and confirm that session actually belongs to them. It takes the session identifier straight from the request body and passes it to a database update that filters only on the session id and its current status — with no constraint tying the session to the caller.

// approve handler — updates by session id + status only,
// with no check that the session belongs to the caller.
UPDATE emergency_recovery
   SET status = 'ready', ready_at = now()
 WHERE session_id = :sessionIdFromRequestBody
   AND status = 'waiting';

So any known waiting session can be flipped to ready by anyone, and the ready state resets the wait timer to now. Downstream, the safety check only enforces the waiting period while the status is still waiting. Once the row is flipped to ready, that check is simply skipped. The password-change step then proceeds, and as part of completing recovery it disables the victim's TOTP two-factor and removes their passkey two-factor, with no further ownership check.

The attacker can already decrypt the victim's recovery key, because during the legitimate emergency-contact setup the victim's key was deliberately wrapped to the attacker's public key. That is the intended design — the waiting period was the only thing keeping it in check.

There is also a smaller companion issue: the "recovery approved" notification is addressed using the attacker-supplied identifier rather than the real session owner, so the notification goes to the attacker instead of the victim. The victim gets no signal that their safety window was just skipped.

Exploitation

Prerequisite: the attacker was, at some earlier point, legitimately added as the victim's emergency contact and accepted. This is a normal, expected use of the feature — not something the attacker has to trick the victim into at attack time.

The chain runs in seconds:

  1. The attacker logs in normally and starts a recovery for the victim's account. The system creates a waiting session with a 30-day timer and emails the victim. The attacker does not wait.
  2. The attacker reads back their own recovery sessions through a normal info endpoint, which returns the new session's identifier.
  3. The bug: the attacker calls the approve-recovery endpoint with that session id, claiming to approve as themselves. The missing ownership check lets it through, and the session flips from waiting to ready with the wait timer reset to now.
  4. The attacker initiates and then completes a password change on the victim's account. Ente's recovery flow disables the victim's TOTP and passkey two-factor as part of the process, and the victim's credentials are replaced.

At the end of this, the attacker controls the account. The victim may not even have opened the email that was supposed to warn them.

Impact

The reason this is more than a single-app compromise: whoever takes over an Ente Auth account can read the 2FA seeds that guard the victim's other accounts. The blast radius extends well past Ente.

CVSS vector rationale

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Remediation

The vendor fixed the issue in PR #11311. The fix loads the recovery session and verifies it belongs to the account and contact named in the request before allowing the status change, and enforces that the session is genuinely in the waiting state.

  1. Hosted users on api.ente.io are already covered by the vendor patch.
  2. Self-hosted operators should update to a build that includes PR #11311.
  3. Audit recovery records for sessions that moved from waiting to ready faster than the configured notice period — that timing gap is the fingerprint of this bug being exercised.

Disclosure timeline

References

Found and reported by Louis Sanchez, Founder & Principal Security Consultant at Voke Cyber (OSCP, OSWA, CISSP, CCSK). Prior advisories: Cal.com webhook plant, CVE-2026-15630 (Casdoor), CVE-2026-39878 (Chamilo), CVE-2026-48742 (Coolify), and CVE-2026-35198 (HeyForm).

We find the bugs tools miss

A recovery flow can look correct at every individual step and still hand over an account, if one transition trusts an identifier it never verified. Finding that takes reading the state machine and asking, at each hop, whether the object being acted on belongs to the caller. That is how we approach web application and API testing for clients across the Charlotte, NC area and nationwide.

Get a Quote