Confirmed DuplicateKeyException not caught during Connected Account registration (username conflict)

Eris Greyrat

Well-known member
Licensed customer
Affected version
2.3.9
When a user attempts to register via a connected account at GitHub OAuth, and the chosen username already exists in xf_user, XenForo throws an unhandled XF\Db\DuplicateKeyException (MySQL error 1062) instead of returning a user-friendly validation error.

The registration form accepts the input, passes validation, and only fails at the INSERT stage, resulting in a logged server error visible to admins and a broken experience for the end user.

Steps to reproduce:​

  1. Have an existing account with username X already registered.
  2. Begin OAuth registration via a connected account provider GitHub.
  3. On the registration completion form (/register/connected-accounts/github/register), manually enter username X (the conflicting one).
  4. Click the submit button.

Expected behaviour:​

A validation error is returned ("That username is already taken."), and the user remains on the registration form.

Actual behaviour:​

The form submission proceeds past validation, attempts the INSERT INTO xf_user, and throws:
Code:
XF\Db\DuplicateKeyException: MySQL query error [1062]: Duplicate entry 'X' for key 'username'
src/XF/Db/AbstractStatement.php:225

Relevant stack trace:​

Code:
#4  src/XF/Mvc/Entity/Entity.php(1578):   XF\Db\AbstractAdapter->insert('xf_user', ...)
#5  src/XF/Mvc/Entity/Entity.php(1306):   XF\Mvc\Entity\Entity->_saveToSource()
#6  src/XF/Service/User/RegistrationService.php(325): XF\Mvc\Entity\Entity->save()
#7  src/XF/Service/ValidateAndSavableTrait.php(42):   XF\Service\User\RegistrationService->_save()
#8  src/XF/Pub/Controller/RegisterController.php(345): XF\Service\User\RegistrationService->save()
#9  src/XF/Mvc/Dispatcher.php(362): XF\Pub\Controller\RegisterController->actionConnectedAccountRegister(...)

Root cause (suspected):​

The connected account registration path (actionConnectedAccountRegister) either:
  • Does not run the same username uniqueness check that the standard registration flow performs, or
  • Is susceptible to a TOCTOU race between validation and the actual INSERT.
Either way, RegistrationService::_save() does not catch \XF\Db\DuplicateKeyException and convert it into a user-facing validation error, which it should.

Environment:​

  • Connected account provider: GitHub
  • Triggered route: /register/connected-accounts/github/register
  • Error class: XF\Db\DuplicateKeyException
  • MySQL error: 1062 Duplicate entry for key username

​

Fix:
RegistrationService::_save() (or the ValidateAndSavableTrait) should catch \XF\Db\DuplicateKeyException on the username key and surface it as a proper validation error. Alternatively, the connected account registration flow should explicitly re-validate username uniqueness immediately before calling save().
 
There is also a process that happens via the X (Twitter) connected-account provider, not GitHub-specific.

Just hit this exact issue on XF 2.3.x, but through the X (Twitter) OAuth flow rather than GitHub, which suggests the root cause is in the shared RegistrationService::_save() path rather than anything provider-specific.

Same failure point: user submits /register/connected-accounts/x/register with a username that already exists in xf_user. Validation passes, the form submits, and the INSERT throws an uncaught exception XF\Db\DuplicateKeyException instead of surfacing a "that username is already taken" validation error.

Code:
XF\Db\DuplicateKeyException: MySQL query error [1062]: Duplicate entry 'YEZHI' for key 'username'
src/XF/Db/AbstractStatement.php:225

Stack trace:
#0 src/XF/Db/Mysqli/Statement.php(207): XF\Db\AbstractStatement->getException('MySQL query err...', 1062, '23000')
#1 src/XF/Db/Mysqli/Statement.php(83): XF\Db\Mysqli\Statement->getException('MySQL query err...', 1062, '23000')
#2 src/XF/Db/AbstractAdapter.php(96): XF\Db\Mysqli\Statement->execute()
#3 src/XF/Db/AbstractAdapter.php(219): XF\Db\AbstractAdapter->query('INSERT  INTO `x...', Array)
#4 src/XF/Mvc/Entity/Entity.php(1578): XF\Db\AbstractAdapter->insert('xf_user', Array, false)
#5 src/XF/Mvc/Entity/Entity.php(1306): XF\Mvc\Entity\Entity->_saveToSource()
#6 src/XF/Service/User/RegistrationService.php(325): XF\Mvc\Entity\Entity->save()
#7 src/XF/Service/ValidateAndSavableTrait.php(42): XF\Service\User\RegistrationService->_save()
#8 src/XF/Pub/Controller/RegisterController.php(345): XF\Service\User\RegistrationService->save()
#9 src/XF/Mvc/Dispatcher.php(362): XF\Pub\Controller\RegisterController->actionConnectedAccountRegister(Object(XF\Mvc\ParameterBag))
#10 src/XF/Mvc/Dispatcher.php(264): XF\Mvc\Dispatcher->dispatchFromMatch(Object(XF\Mvc\RouteMatch), Object(XF\Pub\Controller\RegisterController), NULL)
#11 src/XF/Mvc/Dispatcher.php(121): XF\Mvc\Dispatcher->dispatchFromMatch(Object(XF\Mvc\RouteMatch), Object(XF\Pub\Controller\RegisterController), NULL)
#12 src/XF/Mvc/Dispatcher.php(63): XF\Mvc\Dispatcher->dispatchLoop(Object(XF\Mvc\RouteMatch))
#13 src/XF/App.php(2824): XF\Mvc\Dispatcher->run()
#14 src/XF.php(814): XF\App->run()
#15 index.php(23): XF::runApp('XF\\Pub\\App')
#16 {main}

Relevant POST data, username submitted matched an existing account exactly:

Code:
["username"] => string(5) "YEZHI"

Same as the original first post issue because this issue looks like a TOCTOU gap between the pre-submit validation and the actual xf_user INSERT, compounded by RegistrationService::_save() not catching XF\Db\DuplicateKeyException. Given it reproduces across at least two different connected-account providers now, it seems safe to say the fix belongs in the shared registration save path (or a pre-save() uniqueness re-check) rather than in any individual provider integration.
 
Automated triage: Awaiting feedback

Both registration forms run the same duplicate-username check, and in our testing a name that already exists is rejected with a normal error before anything is saved. We need a little more detail to look into this.

  • Does this happen every time you submit an existing username once on the GitHub registration form, or did you find the error in the server error log?
  • Does it still happen with all add-ons disabled? If not, which add-ons are installed that change user registration or usernames?
  • Could someone else, or another browser tab, have been registering the same username at the same moment the error was logged?

This is an automated assessment. A member of staff may review and update it.
 
Automated triage: Awaiting feedback

Both registration forms run the same duplicate-username check, and in our testing a name that already exists is rejected with a normal error before anything is saved. We need a little more detail to look into this.

  • Does this happen every time you submit an existing username once on the GitHub registration form, or did you find the error in the server error log?
  • Does it still happen with all add-ons disabled? If not, which add-ons are installed that change user registration or usernames?
  • Could someone else, or another browser tab, have been registering the same username at the same moment the error was logged?

This is an automated assessment. A member of staff may review and update it.
Oh AI XF?

This is the same issue applied to all GitHub, Google, and X (Twitter)

Code:
Server error log
[LIST]
[*]XF\Db\DuplicateKeyException: MySQL query error [1062]: Duplicate entry 'huyanluanyu' for key 'username'
[*]src/XF/Db/AbstractStatement.php:225
[*]Generated by: Unknown account
[*]Oct 7, 2026 at 08:16
[/LIST]
[HEADING=2]Stack trace[/HEADING]
INSERT  INTO `xf_user` (`visible`, `activity_visible`, `user_group_id`, `timezone`, `language_id`, `last_summary_email_date`, `username`, `email`, `user_state`, `privacy_policy_accepted`, `terms_accepted`, `display_style_group_id`, `secondary_group_ids`, `secret_key`, `user_id`, `username_date`, `username_date_visible`, `style_id`, `style_variation`, `permission_combination_id`, `message_count`, `question_solution_count`, `alerts_unviewed`, `alerts_unread`, `conversations_unread`, `register_date`, `last_activity`, `trophy_points`, `avatar_date`, `avatar_width`, `avatar_height`, `avatar_highdpi`, `avatar_optimized`, `gravatar`, `security_lock`, `is_moderator`, `is_admin`, `is_staff`, `is_banned`, `reaction_score`, `vote_score`, `custom_title`, `warning_points`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
------------

#0 src/XF/Db/Mysqli/Statement.php(207): XF\Db\AbstractStatement->getException('MySQL query err...', 1062, '23000')
#1 src/XF/Db/Mysqli/Statement.php(83): XF\Db\Mysqli\Statement->getException('MySQL query err...', 1062, '23000')
#2 src/XF/Db/AbstractAdapter.php(96): XF\Db\Mysqli\Statement->execute()
#3 src/XF/Db/AbstractAdapter.php(219): XF\Db\AbstractAdapter->query('INSERT  INTO `x...', Array)
#4 src/XF/Mvc/Entity/Entity.php(1578): XF\Db\AbstractAdapter->insert('xf_user', Array, false)
#5 src/XF/Mvc/Entity/Entity.php(1306): XF\Mvc\Entity\Entity->_saveToSource()
#6 src/XF/Service/User/RegistrationService.php(325): XF\Mvc\Entity\Entity->save()
#7 src/XF/Service/ValidateAndSavableTrait.php(42): XF\Service\User\RegistrationService->_save()
#8 src/XF/Pub/Controller/RegisterController.php(345): XF\Service\User\RegistrationService->save()
#9 src/XF/Mvc/Dispatcher.php(362): XF\Pub\Controller\RegisterController->actionConnectedAccountRegister(Object(XF\Mvc\ParameterBag))
#10 src/XF/Mvc/Dispatcher.php(264): XF\Mvc\Dispatcher->dispatchClass('XF:Register', 'ConnectedAccoun...', Object(XF\Mvc\RouteMatch), Object(XF\Pub\Controller\RegisterController), NULL)
#11 src/XF/Mvc/Dispatcher.php(121): XF\Mvc\Dispatcher->dispatchFromMatch(Object(XF\Mvc\RouteMatch), Object(XF\Pub\Controller\RegisterController), NULL)
#12 src/XF/Mvc/Dispatcher.php(63): XF\Mvc\Dispatcher->dispatchLoop(Object(XF\Mvc\RouteMatch))
#13 src/XF/App.php(2834): XF\Mvc\Dispatcher->run()
#14 src/XF.php(814): XF\App->run()
#15 index.php(23): XF::runApp('XF\\Pub\\App')
#16 {main}
[HEADING=2]Request state[/HEADING]
array(4) {
  ["url"] => string(44) "/register/connected-accounts/google/register"
  ["referrer"] => string(57) "https://domain.com/register/connected-accounts/google/"
  ["_GET"] => array(0) {
  }
  ["_POST"] => array(9) {
    ["_xfToken"] => string(8) "********"
    ["username"] => string(11) "huyanluanyu"
    ["dob_month"] => string(2) "11"
    ["dob_day"] => string(2) "22"
    ["dob_year"] => string(4) "2000"
    ["custom_fields"] => array(1) {
      ["gender"] => string(4) "none"
    }
    ["accept"] => string(1) "1"
    ["_xfRedirect"] => string(66) "https://domain.com/mods/eris.greyrat.character.9503/"
    ["timezone"] => string(14) "Asia/Hong_Kong"
  }
}
 
Automated triage: Confirmed

Thank you for reporting this issue. The extra logs help: they show the same error through GitHub, X and Google, so it isn't specific to one provider. Both registration forms check that the username isn't taken, but that check can't account for another registration of the same name that is still being saved. When that happens, the database rejects the second account and the error isn't turned into the normal "The specified username is already in use" message, which is what this report is now confirmed for.

To check that this is what happened on your site: for one of the logged errors, such as huyanluanyu on Oct 7 at 08:16, was the existing account with that name registered at almost the same time, for example by the same person submitting twice? If that account is much older, please let us know, as something else on your site, such as an add-on, would then be involved.

This is an automated assessment. A member of staff may review and update it.
 
Hello. It's me. A human. Apologies for the previous verdict of "Awaiting feedback". We've been building the tooling for this and iterating on it over the last week and we've applied improvements as we've gone and while most verdicts are reasonable, some may have benefited from the recent changes, as was the case here.

A second look was able to see the replies you'd made since the first post and the additional context leads us to believe there is a bug here, although the circumstances are still somewhat confusing.

The bot seems to think the duplicate is happening due to two users registering simultaneously, but I believe you're describing a scenario whereby "User A" already exists and has done for some time, and then another user comes along to register with a connected account and also opts to be called "User A".

I'm not currently able to reproduce the issue myself so I'm still unsure if there's more at play outside of the core code.
 
Do you not see the "Usernames must be unique" banner immediately after typing a duplicate username?

1791469239241.webp

This is the behaviour I see, and the expected behaviour:

1791469281048.webp

I feel like there's something missing here so if you can elaborate that would be appreciated.
 
Chris D said:
Do you not see the "Usernames must be unique" banner immediately after typing a duplicate username?

Yes, I see it. If the name already belongs to an older account, the form rejects it fine.

The errors I got are from a different case. Two people (or the same person in two tabs) register the same new username at the same time. Both requests pass the username check, because neither one can see the other user yet. The first one saves, and the second one fails on the INSERT with the duplicate key error. Nothing catches that exception, so the user gets the "Oops! We ran into some problems." page and it goes into the error log.

I tested it on a clean 2.3.9 by running two registrations with the same name at the same moment. 29 out of 30 tries threw the DuplicateKeyException. An existing old username was always rejected normally.

No data is broken, the first account is fine. It is only the error page and the log entry.

This fixed it for me:

Diff:
--- a/src/XF/Service/User/RegistrationService.php
+++ b/src/XF/Service/User/RegistrationService.php
@@ -3,7 +3,9 @@
 namespace XF\Service\User;

 use XF\CustomField\Set;
+use XF\Db\DuplicateKeyException;
 use XF\Entity\User;
+use XF\PrintableException;
 use XF\Repository\ChangeLogRepository;
 use XF\Repository\IpRepository;
 use XF\Repository\PreRegActionRepository;
@@ -322,7 +324,20 @@
     {
         $user = $this->user;

-        $user->save();
+        try
+        {
+            $user->save();
+        }
+        catch (DuplicateKeyException $e)
+        {
+            // Another registration took this name after our duplicate checks ran (concurrent submit).
+            if (strpos($e->getMessage(), 'username') === false)
+            {
+                throw $e;
+            }
+
+            throw new PrintableException(\XF::phrase('usernames_must_be_unique'));
+        }

         $this->app->spam()->userChecker()->logSpamTrigger('user', $user->user_id);

With this, the second user just gets "Usernames must be unique" like normal, and nothing is logged.
 
How often is this happening outside of you forcing it to happen?
Quite often. These were real users signing up with Google, GitHub, and X on my live site, not something I forced. I only reproduced it on my test site to find the cause.

After I applied the change above, the errors stopped.
 
Back
Top Bottom