Forums Bug Reports Thread

Email-change finalize does not re-check email uniqueness (TOCTOU) — surfaces a raw 500 instead of a clean error when the address is taken between request and finalize

Patrick Bass · Jun 6 · 16 · 1 Locked
[Minor] [Normal Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 9:35pm

Area: Account (re-run) (audit p1r) · Surface: POST /account/email/finalize (AccountController@finalizeEmailChange) · Dimension: security · Severity: minor

Between starting an email change and completing the multi-step code verification (a 30-minute window), the target email can be claimed by another account (e.g. that account changes its own email to this value, or a new signup). At finalize, the controller does not re-verify uniqueness and issues a bare UPDATE that violates the unique index, producing a raw 500 error rather than a graceful 'that email is now in use' message. Time-of-check/time-of-use gap (OWASP A04:2021 Insecure Design). Low security impact because the DB constraint prevents actual duplication, but it is a poor failure mode on a security-sensitive flow.

Evidence

Uniqueness is checked only at request time: AccountController.php:903-911 (`SELECT id FROM users WHERE email = :email AND id != :id` in changeEmail). finalizeEmailChange (AccountController.php:1044-1052) blindly runs `UPDATE users SET email = :email ... WHERE id = :id` with no re-check. The users table has UNIQUE KEY `uq_users_email` (schema.sql:7483), so if the target email was claimed by another account between the start and finalize of the flow, the UPDATE throws a PDOException. There is no try/catch here, so it bubbles to the global handler as a 500.

Suggested fix. In finalizeEmailChange, re-run the `email = :email AND id != :id` uniqueness check immediately before the UPDATE and flash a clean error if taken; additionally wrap the UPDATE in a try/catch for the duplicate-key PDOException so the user never sees a 500.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:49am

Resolved — fixed and deployed. Commit ea9f0311e960, shipped dev-first then to all tenants on 2026-06-06.

In finalizeEmailChange() added a re-run of the `email = :email AND id != :id` uniqueness SELECT immediately before the UPDATE (clears the pending session, flashes a clean 'already in use' error, and redirects to /account/settings?tab=email if taken). Also wrapped the UPDATE in try/catch for PDOException code 23000 (duplicate-key) so a concurrent claim between SELECT and UPDATE yields the same clean error instead of a 500.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.