phpMyFAQ: Microsoft Entra ID (Azure) Login Hijacks Pre-Existing Local Accounts via Mutable preferred_username Claim Without Immutable Subject Binding
What fixes it
- 4.2.0-betaphpMyFAQ, phpGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary :
phpMyFAQ's Microsoft Entra ID authentication flow links users based on the mutable preferred_username claim instead of an immutable identity identifier such as oid or sub.
When an Entra-authenticated user has a preferred_username that matches an existing local phpMyFAQ username, User::createUser() attempts to create the account but first loads the already existing local user object.
The duplicate-user exception is then caught and ignored by AuthEntraId::create(). Execution continues using the already-loaded local account object, after which the application activates the account, changes its authentication source to Azure, overwrites the email address, and ultimately logs the Entra identity into the pre-existing local account.
This may result in complete account takeover, including takeover of a local super-admin account, when an attacker can control an Entra identity whose preferred_username collides with the targeted phpMyFAQ local username.
Severity :
High
CVSS 3.1:
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Score: 8.1 (High)
CWE:
- CWE-287 โ Improper Authentication
- CWE-290 โ Authentication Bypass by Spoofing
- CWE-863 โ Incorrect Authorization
Confidence:
Medium
The vulnerable code path has been confirmed. Practical exploitability depends on whether an attacker can control an identity within the configured Microsoft Entra tenant whose preferred_username matches an existing phpMyFAQ local login.
Affected Version :
phpMyFAQ 4.2.x
Reviewed target:
4.2.0-beta development line
Git revision reviewed:
0fb7ecd
Affected Component :
Microsoft Entra ID / Azure frontend authentication flow.
Affected files include:
phpmyfaq/src/phpMyFAQ/Auth/AuthEntraId.php
phpmyfaq/src/phpMyFAQ/Auth/EntraId/OAuth.php
phpmyfaq/src/phpMyFAQ/Controller/Frontend/AzureAuthenticationController.php
phpmyfaq/src/phpMyFAQ/User.php
Primary affected functions/classes:
AuthEntraId::create()
OAuth::getMail()
User::createUser()
User::getUserByLogin()
Azure authentication callback:
GET /auth/azure/callback?code=...
Description :
The Microsoft Entra authentication implementation uses the preferred_username value from the Entra ID token as the local phpMyFAQ login identifier.
OAuth::getMail() returns:
public function getMail(): string
{
return (string) ($this->token->preferred_username ?? '');
}During the Azure authentication process, the value is eventually passed into:
$user->createUser($login, '', $domain);
where $login is derived from preferred_username.
If a local user already exists with the same login name, User::getUserByLogin() retrieves that user and populates the current $user object using the existing database record.
createUser() subsequently detects the duplicate login and throws a LOGIN_NOT_UNIQUE exception.
However, AuthEntraId::create() catches and ignores the exception:
try {
$result = $user->createUser($login, '', $domain);
} catch (\Exception $exception) {
$this->configuration->getLogger()->info($exception->getMessage());
}The application then continues operating on the $user object that already contains the existing victim's identity.
The following actions are then performed:
$user->setStatus('active');
$user->setAuthSource(
AuthenticationSourceType::AUTH_AZURE->value
);
$user->setUserData([
'display_name' => $this->oAuth->getName(),
'email' => $this->oAuth->getMail()
]);As a result, the pre-existing local account can be modified as a side effect of an Entra login collision.
The account can be:
- activated even if it was previously blocked;
- converted to Azure authentication;
- assigned the attacker's Entra email address;
- authenticated as the existing local user.
There is no persistent binding between the local phpMyFAQ account and an immutable Microsoft Entra identifier such as oid or sub.
This differs from the application's Keycloak implementation, which links identities using a stored immutable subject identifier.
Proof of Concept :
Prerequisites:
- Microsoft Entra / Azure authentication must be enabled in phpMyFAQ.
- A local phpMyFAQ account must already exist, for example:
admin
- The tester must control a Microsoft Entra identity in the configured test tenant whose token contains:
{
"preferred_username": "admin"
}- Authenticate through the legitimate Microsoft Entra login flow.
- Complete:
/auth/azure/callback
with the authorization code returned by Microsoft.
Expected secure behavior:
The Entra identity should either:
- be rejected because it is not explicitly linked to the existing local account, or
- create a separate SSO identity associated with its immutable
oid/sub.
Observed vulnerable behavior:
The existing local account matching preferred_username is loaded.
The duplicate-account exception is swallowed.
The existing account object remains active in memory and is subsequently modified.
The account receives:
account_status = active auth_source = azure email = attacker's preferred_username
The Azure-authenticated user is then treated as the pre-existing local phpMyFAQ account.
Steps To Reproduce :
- Deploy a phpMyFAQ 4.2.x test instance.
- Configure Microsoft Entra ID authentication against a controlled test tenant.
- Create a local phpMyFAQ account:
Username: victim
For higher-impact validation, the account may be assigned administrative privileges within the controlled testing environment.
- Create or configure an Entra identity whose ID token contains:
preferred_username = victim
while its immutable Microsoft identifier (oid or sub) belongs to a different identity that has never previously been linked to the local phpMyFAQ account.
- Start the Microsoft authentication process through phpMyFAQ.
- Authenticate using the controlled Entra identity.
- Allow Microsoft to redirect the browser to:
GET /auth/azure/callback?code=...
- The callback validates the Microsoft token and extracts:
preferred_username
- phpMyFAQ calls:
AuthEntraId::checkCredentials()
followed by:
AuthEntraId::create($login)
User::createUser("victim")calls:
User::getUserByLogin("victim", false)- The existing local victim account is loaded into the
$userobject.
createUser()detects the duplicate login and throws an exception.
AuthEntraId::create()catches and ignores this exception.
- Execution continues on the already-loaded victim account.
- phpMyFAQ executes operations equivalent to:
$user->setStatus('active');
$user->setAuthSource('azure');
$user->setUserData(...);- The authentication controller performs another lookup using the same
preferred_username.
- The Azure-authenticated session is established as the existing local
victimaccount.
- Verify that the victim account has been modified:
status -> active auth_source -> azure email -> attacker's Entra value
- Verify that the Entra identity now has access to the permissions belonging to the original local account.
Impact :
Successful exploitation results in authentication as an existing local phpMyFAQ user without knowing that user's password.
The impact includes:
Account Takeover
An attacker-controlled Entra identity may become authenticated as an existing local account.
Administrative Account Takeover
If the colliding username belongs to an administrator or super-admin, the attacker may inherit the privileges of that account.
Blocked Account Reactivation
The vulnerable flow explicitly performs:
$user->setStatus('active');Therefore an account that was intentionally disabled may become active again.
Authentication Source Modification
The victim account authentication source is changed to Azure:
auth_source = azure
which modifies the security configuration of an existing local account.
Email Address Modification
The account's email data is overwritten using information obtained from the Entra identity.
This may create additional account-recovery or password-reset consequences.
Persistent Account Mutation
The vulnerability is not limited to temporary authentication. The Entra login process modifies stored properties of the victim account.
Confidentiality impact: High
Integrity impact: High
Availability impact: None
Root Causes :
The primary root cause is treating the mutable Microsoft Entra:
preferred_username
claim as an authoritative account identifier.
preferred_username is not an immutable identity key and should not be used to permanently associate an external identity with an existing security principal.
The vulnerability is compounded by several implementation issues:
- No immutable
oid/subbinding exists between the Entra identity and the local phpMyFAQ account.
User::createUser()attempts to resolve an existing account before throwing the duplicate-login exception.
- Resolving the existing account mutates the active
$userobject by loading the victim's database record into it.
AuthEntraId::create()catches and ignores the duplicate-user exception.
- Execution continues using the mutated
$userobject.
- SSO authentication performs security-sensitive modifications including:
setStatus('active')
setAuthSource('azure')
setUserData(...)- Existing local accounts may therefore be automatically adopted based only on a mutable username collision.
Suggested Fix :
Microsoft Entra identities should be associated with phpMyFAQ accounts exclusively through immutable subject identifiers.
Use a persistent value such as:
oid
or:
sub
and store the identifier in a dedicated field such as:
entra_sub
The authentication flow should resemble:
$sub = (string) (
$this->token->oid
?? $this->token->sub
?? ''
);
$userId = $this->userRepo->findUserIdByEntraSubject($sub);
if ($userId === null) {
$userId = $this->provisionNewSsoUser($sub, $claims);
}
$user = CurrentUser::getById($userId);The implementation should never automatically link an external Entra identity to an existing local account based solely on username or email.
If account linking is required, it should require an explicit authenticated account-linking workflow where the existing local user confirms ownership.
Additionally:
- Do not automatically call
setStatus('active')during normal SSO authentication. - Do not automatically convert an existing account's
auth_source. - Do not overwrite the existing account's email address merely because an SSO login occurred.
- Reject identity collisions instead of silently adopting the local account.
- Store the immutable Entra subject when a new SSO account is provisioned.
- Log and alert on external-identity linking operations.
- Ensure local administrator authentication remains independent of SSO identity collisions.
- Validate the OpenID Connect
nonce. - Validate OAuth/OIDC
stateduring the callback.
A regression test should verify the following condition:
Given:
local username = admin
and a valid Microsoft Entra ID token containing:
preferred_username = admin oid = previously-unlinked-identity
the request must:
NOT authenticate as admin NOT activate admin NOT change admin.auth_source NOT change admin.email
The application should instead reject the identity collision or provision a separate explicitly linked SSO account.
References :
Affected source code:
phpmyfaq/src/phpMyFAQ/Auth/AuthEntraId.phpphpmyfaq/src/phpMyFAQ/Auth/EntraId/OAuth.phpphpmyfaq/src/phpMyFAQ/Controller/Frontend/AzureAuthenticationController.phpphpmyfaq/src/phpMyFAQ/User.php
Relevant reviewed locations:
AuthEntraId.php:63-84AuthEntraId.php:69-81OAuth.php:233-236AzureAuthenticationController.php:122-135User::createUser()User::getUserByLogin()User::extractUserFromResult()
Weakness classifications:
- CWE-287: Improper Authentication
- CWE-290: Authentication Bypass by Spoofing
- CWE-863: Incorrect Authorization
Authentication standard considerations:
- OpenID Connect immutable subject identifiers (
sub) - Microsoft Entra immutable identity identifiers (
oid) - Microsoft identity platform ID-token claim guidance regarding
preferred_username
Related implementation comparison:
phpMyFAQ's Keycloak authentication path already demonstrates the safer design by linking local accounts through a stored immutable keycloak_sub rather than automatically adopting accounts by username/email.
Text from GitHub Advisory Database
Affected versions 1
- phpMyFAQ, php<= 4.2.0-alpha.2GitHub