Code reading on 28 September 2026 at 124849b (development). Unverified: not checked live, and the effect below is read from code only. Found in the development → beta security review. This is new in the delta: beta is not affected.
What happens
Someone holding only a user's session or app password can end up with that user's new private key after the user runs compromise recovery. No master password is needed. The attacker does need a second Nextcloud account with a keepiq suite on the same instance.
POST /api/v1/emergency-access/contacts has no vault-key proof. EmergencyAccessController::create is #[NoAdminRequired] only, unlike destroy. EmergencyAccessService::designate() checks only that the envelope is non-empty. It then records grantorSuiteId as the victim's current active suite (lib/Service/EmergencyAccessService.php, designate()). So the attacker, acting as the victim, adds their own account as a contact with a garbage envelope.
- The attacker calls
request straight away and turns off the victim's notify_security preference. That preference is a user setting written through settings#updateUserSettings, which needs no proof. After the shortest wait period (1 day, which the attacker chose), promoteIfElapsed() moves the contact to approved.
- The victim suspects a compromise and runs compromise recovery.
initiateCompromiseRecovery calls migrateEmergencyContacts (src/store/modules/encryptionSuite.js, about lines 371–436). That function wraps the new private key to every contact whose grantorSuiteId is the old suite, without asking the user. On the server, EmergencyEnvelopeInvalidationService::reEnvelopeForRotation() keeps the approved state.
EmergencyAccessService::fetchEnvelope() returns the envelope as soon as the state is approved. The attacker now holds the new private key.
On beta, rotation invalidated every envelope on the old suite, so a planted contact only ever held garbage. The automatic re-escrow added by #674 is what makes the planted contact dangerous.
Proposed fix
- Require a vault-key proof on
emergencyAccess#create.
- During rotation, do not carry
requested or approved contacts across silently (refuse them on the server as well). Show the user the contacts that will receive the new key, and ask them to confirm first.
- Separately: stop the security notifications from being switched off with a session alone.
Live check
Using user A's app password, create a contact for B, request it, and wait until it is approved. Then, as A in the browser, run compromise recovery. Finally, as B, fetch the envelope and try to decrypt it with B's key.
Code reading on 28 September 2026 at 124849b (development). Unverified: not checked live, and the effect below is read from code only. Found in the development → beta security review. This is new in the delta: beta is not affected.
What happens
Someone holding only a user's session or app password can end up with that user's new private key after the user runs compromise recovery. No master password is needed. The attacker does need a second Nextcloud account with a keepiq suite on the same instance.
POST /api/v1/emergency-access/contactshas no vault-key proof.EmergencyAccessController::createis#[NoAdminRequired]only, unlikedestroy.EmergencyAccessService::designate()checks only that the envelope is non-empty. It then recordsgrantorSuiteIdas the victim's current active suite (lib/Service/EmergencyAccessService.php,designate()). So the attacker, acting as the victim, adds their own account as a contact with a garbage envelope.requeststraight away and turns off the victim'snotify_securitypreference. That preference is a user setting written throughsettings#updateUserSettings, which needs no proof. After the shortest wait period (1 day, which the attacker chose),promoteIfElapsed()moves the contact toapproved.initiateCompromiseRecoverycallsmigrateEmergencyContacts(src/store/modules/encryptionSuite.js, about lines 371–436). That function wraps the new private key to every contact whosegrantorSuiteIdis the old suite, without asking the user. On the server,EmergencyEnvelopeInvalidationService::reEnvelopeForRotation()keeps theapprovedstate.EmergencyAccessService::fetchEnvelope()returns the envelope as soon as the state isapproved. The attacker now holds the new private key.On beta, rotation invalidated every envelope on the old suite, so a planted contact only ever held garbage. The automatic re-escrow added by #674 is what makes the planted contact dangerous.
Proposed fix
emergencyAccess#create.requestedorapprovedcontacts across silently (refuse them on the server as well). Show the user the contacts that will receive the new key, and ask them to confirm first.Live check
Using user A's app password, create a contact for B, request it, and wait until it is approved. Then, as A in the browser, run compromise recovery. Finally, as B, fetch the envelope and try to decrypt it with B's key.