Area: Account (re-run) (audit p1r) · Surface: GET /account/verified/confirm-email (VerificationController@confirmEmail) · Dimension: security · Severity: minor
The email-confirmation token is treated as proof of email ownership but is never tied to the authenticated session user, and on any lookup failure the controller still advances whoever is logged in to the selfie-upload step of their own verification. A 64-char string that fails the API lookup is enough to push the current user into completing verification. Combined with the GET being state-changing (it flips email_verified_at), this is a weak binding between the token, the email recipient, and the session — OWASP A01:2021 / A07:2021. Impact is limited because the platform-admin side ultimately reviews submissions, but the confirm step should bind token→user.
Evidence
VerificationController.php:247-301. The token from the email link is looked up via PlatformAdminClient (`/api/verification/by-token?token=...`) but the returned verification is never checked to belong to the current session user before marking it email-verified (lines 262-270: `PlatformAdminClient::doPost('/api/verification/update', ['id' => $vid, 'email_verified_at' => ...])`). On the failure branch (vid <= 0) it calls `$this->requireAuth()` and forwards the user to `/account/verified/selfie?vid=...` for THEIR current verification regardless of whether the clicked token was valid (lines 271-284), with only a length check (line 250 `strlen($token) !== 64`) gating entry.
Suggested fix. After resolving the token, verify the returned verification's user_id (zitadel_sub) matches the authenticated user before marking email_verified_at; on lookup failure do NOT auto-advance to the selfie step — show an explicit 'invalid or expired link' error and require the user to restart from /account/verified.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus