{"schema":"threatline.record/2","record":{"id":"GHSA-6hhv-gc4g-f4r7","aliases":["GHSA-6hhv-gc4g-f4r7"],"summary":"Microsoft Entra ID (Azure) Login Hijacks Pre-Existing Local Accounts via Mutable preferred_username Claim Without Immutable Subject Binding","details":"Summary :\r\n\r\nphpMyFAQ'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`.\r\n\r\nWhen 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.\r\n\r\nThe 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.\r\n\r\nThis 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.\r\n\r\nSeverity :\r\n\r\nHigh\r\n\r\nCVSS 3.1:\r\n\r\n`CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N`\r\n\r\nScore: **8.1 (High)**\r\n\r\nCWE:\r\n\r\n* CWE-287 — Improper Authentication\r\n* CWE-290 — Authentication Bypass by Spoofing\r\n* CWE-863 — Incorrect Authorization\r\n\r\nConfidence:\r\n\r\nMedium\r\n\r\nThe 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.\r\n\r\nAffected Version :\r\n\r\nphpMyFAQ **4.2.x**\r\n\r\nReviewed target:\r\n\r\n`4.2.0-beta` development line\r\n\r\nGit revision reviewed:\r\n\r\n`0fb7ecd`\r\n\r\nAffected Component :\r\n\r\nMicrosoft Entra ID / Azure frontend authentication flow.\r\n\r\nAffected files include:\r\n\r\n`phpmyfaq/src/phpMyFAQ/Auth/AuthEntraId.php`\r\n\r\n`phpmyfaq/src/phpMyFAQ/Auth/EntraId/OAuth.php`\r\n\r\n`phpmyfaq/src/phpMyFAQ/Controller/Frontend/AzureAuthenticationController.php`\r\n\r\n`phpmyfaq/src/phpMyFAQ/User.php`\r\n\r\nPrimary affected functions/classes:\r\n\r\n`AuthEntraId::create()`\r\n\r\n`OAuth::getMail()`\r\n\r\n`User::createUser()`\r\n\r\n`User::getUserByLogin()`\r\n\r\nAzure authentication callback:\r\n\r\n`GET /auth/azure/callback?code=...`\r\n\r\nDescription :\r\n\r\nThe Microsoft Entra authentication implementation uses the `preferred_username` value from the Entra ID token as the local phpMyFAQ login identifier.\r\n\r\n`OAuth::getMail()` returns:\r\n\r\n```php\r\npublic function getMail(): string\r\n{\r\n    return (string) ($this->token->preferred_username ?? '');\r\n}\r\n```\r\n\r\nDuring the Azure authentication process, the value is eventually passed into:\r\n\r\n```php\r\n$user->createUser($login, '', $domain);\r\n```\r\n\r\nwhere `$login` is derived from `preferred_username`.\r\n\r\nIf 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.\r\n\r\n`createUser()` subsequently detects the duplicate login and throws a `LOGIN_NOT_UNIQUE` exception.\r\n\r\nHowever, `AuthEntraId::create()` catches and ignores the exception:\r\n\r\n```php\r\ntry {\r\n    $result = $user->createUser($login, '', $domain);\r\n} catch (\\Exception $exception) {\r\n    $this->configuration->getLogger()->info($exception->getMessage());\r\n}\r\n```\r\n\r\nThe application then continues operating on the `$user` object that already contains the existing victim's identity.\r\n\r\nThe following actions are then performed:\r\n\r\n```php\r\n$user->setStatus('active');\r\n\r\n$user->setAuthSource(\r\n    AuthenticationSourceType::AUTH_AZURE->value\r\n);\r\n\r\n$user->setUserData([\r\n    'display_name' => $this->oAuth->getName(),\r\n    'email' => $this->oAuth->getMail()\r\n]);\r\n```\r\n\r\nAs a result, the pre-existing local account can be modified as a side effect of an Entra login collision.\r\n\r\nThe account can be:\r\n\r\n* activated even if it was previously blocked;\r\n* converted to Azure authentication;\r\n* assigned the attacker's Entra email address;\r\n* authenticated as the existing local user.\r\n\r\nThere is no persistent binding between the local phpMyFAQ account and an immutable Microsoft Entra identifier such as `oid` or `sub`.\r\n\r\nThis differs from the application's Keycloak implementation, which links identities using a stored immutable subject identifier.\r\n\r\nProof of Concept :\r\n\r\nPrerequisites:\r\n\r\n1. Microsoft Entra / Azure authentication must be enabled in phpMyFAQ.\r\n2. A local phpMyFAQ account must already exist, for example:\r\n\r\n```text\r\nadmin\r\n```\r\n\r\n3. The tester must control a Microsoft Entra identity in the configured test tenant whose token contains:\r\n\r\n```json\r\n{\r\n    \"preferred_username\": \"admin\"\r\n}\r\n```\r\n\r\n4. Authenticate through the legitimate Microsoft Entra login flow.\r\n\r\n5. Complete:\r\n\r\n```text\r\n/auth/azure/callback\r\n```\r\n\r\nwith the authorization code returned by Microsoft.\r\n\r\nExpected secure behavior:\r\n\r\nThe Entra identity should either:\r\n\r\n* be rejected because it is not explicitly linked to the existing local account, or\r\n* create a separate SSO identity associated with its immutable `oid`/`sub`.\r\n\r\nObserved vulnerable behavior:\r\n\r\nThe existing local account matching `preferred_username` is loaded.\r\n\r\nThe duplicate-account exception is swallowed.\r\n\r\nThe existing account object remains active in memory and is subsequently modified.\r\n\r\nThe account receives:\r\n\r\n```text\r\naccount_status = active\r\nauth_source    = azure\r\nemail          = attacker's preferred_username\r\n```\r\n\r\nThe Azure-authenticated user is then treated as the pre-existing local phpMyFAQ account.\r\n\r\nSteps To Reproduce :\r\n\r\n1. Deploy a phpMyFAQ 4.2.x test instance.\r\n\r\n2. Configure Microsoft Entra ID authentication against a controlled test tenant.\r\n\r\n3. Create a local phpMyFAQ account:\r\n\r\n```text\r\nUsername: victim\r\n```\r\n\r\nFor higher-impact validation, the account may be assigned administrative privileges within the controlled testing environment.\r\n\r\n4. Create or configure an Entra identity whose ID token contains:\r\n\r\n```text\r\npreferred_username = victim\r\n```\r\n\r\nwhile its immutable Microsoft identifier (`oid` or `sub`) belongs to a different identity that has never previously been linked to the local phpMyFAQ account.\r\n\r\n5. Start the Microsoft authentication process through phpMyFAQ.\r\n\r\n6. Authenticate using the controlled Entra identity.\r\n\r\n7. Allow Microsoft to redirect the browser to:\r\n\r\n```text\r\nGET /auth/azure/callback?code=...\r\n```\r\n\r\n8. The callback validates the Microsoft token and extracts:\r\n\r\n```text\r\npreferred_username\r\n```\r\n\r\n9. phpMyFAQ calls:\r\n\r\n```text\r\nAuthEntraId::checkCredentials()\r\n```\r\n\r\nfollowed by:\r\n\r\n```text\r\nAuthEntraId::create($login)\r\n```\r\n\r\n10. `User::createUser(\"victim\")` calls:\r\n\r\n```text\r\nUser::getUserByLogin(\"victim\", false)\r\n```\r\n\r\n11. The existing local victim account is loaded into the `$user` object.\r\n\r\n12. `createUser()` detects the duplicate login and throws an exception.\r\n\r\n13. `AuthEntraId::create()` catches and ignores this exception.\r\n\r\n14. Execution continues on the already-loaded victim account.\r\n\r\n15. phpMyFAQ executes operations equivalent to:\r\n\r\n```php\r\n$user->setStatus('active');\r\n$user->setAuthSource('azure');\r\n$user->setUserData(...);\r\n```\r\n\r\n16. The authentication controller performs another lookup using the same `preferred_username`.\r\n\r\n17. The Azure-authenticated session is established as the existing local `victim` account.\r\n\r\n18. Verify that the victim account has been modified:\r\n\r\n```text\r\nstatus      -> active\r\nauth_source -> azure\r\nemail       -> attacker's Entra value\r\n```\r\n\r\n19. Verify that the Entra identity now has access to the permissions belonging to the original local account.\r\n\r\nImpact :\r\n\r\nSuccessful exploitation results in authentication as an existing local phpMyFAQ user without knowing that user's password.\r\n\r\nThe impact includes:\r\n\r\n**Account Takeover**\r\n\r\nAn attacker-controlled Entra identity may become authenticated as an existing local account.\r\n\r\n**Administrative Account Takeover**\r\n\r\nIf the colliding username belongs to an administrator or super-admin, the attacker may inherit the privileges of that account.\r\n\r\n**Blocked Account Reactivation**\r\n\r\nThe vulnerable flow explicitly performs:\r\n\r\n```php\r\n$user->setStatus('active');\r\n```\r\n\r\nTherefore an account that was intentionally disabled may become active again.\r\n\r\n**Authentication Source Modification**\r\n\r\nThe victim account authentication source is changed to Azure:\r\n\r\n```text\r\nauth_source = azure\r\n```\r\n\r\nwhich modifies the security configuration of an existing local account.\r\n\r\n**Email Address Modification**\r\n\r\nThe account's email data is overwritten using information obtained from the Entra identity.\r\n\r\nThis may create additional account-recovery or password-reset consequences.\r\n\r\n**Persistent Account Mutation**\r\n\r\nThe vulnerability is not limited to temporary authentication. The Entra login process modifies stored properties of the victim account.\r\n\r\nConfidentiality impact: **High**\r\n\r\nIntegrity impact: **High**\r\n\r\nAvailability impact: **None**\r\n\r\nRoot Causes :\r\n\r\nThe primary root cause is treating the mutable Microsoft Entra:\r\n\r\n```text\r\npreferred_username\r\n```\r\n\r\nclaim as an authoritative account identifier.\r\n\r\n`preferred_username` is not an immutable identity key and should not be used to permanently associate an external identity with an existing security principal.\r\n\r\nThe vulnerability is compounded by several implementation issues:\r\n\r\n1. No immutable `oid` / `sub` binding exists between the Entra identity and the local phpMyFAQ account.\r\n\r\n2. `User::createUser()` attempts to resolve an existing account before throwing the duplicate-login exception.\r\n\r\n3. Resolving the existing account mutates the active `$user` object by loading the victim's database record into it.\r\n\r\n4. `AuthEntraId::create()` catches and ignores the duplicate-user exception.\r\n\r\n5. Execution continues using the mutated `$user` object.\r\n\r\n6. SSO authentication performs security-sensitive modifications including:\r\n\r\n```text\r\nsetStatus('active')\r\nsetAuthSource('azure')\r\nsetUserData(...)\r\n```\r\n\r\n7. Existing local accounts may therefore be automatically adopted based only on a mutable username collision.\r\n\r\nSuggested Fix :\r\n\r\nMicrosoft Entra identities should be associated with phpMyFAQ accounts exclusively through immutable subject identifiers.\r\n\r\nUse a persistent value such as:\r\n\r\n```text\r\noid\r\n```\r\n\r\nor:\r\n\r\n```text\r\nsub\r\n```\r\n\r\nand store the identifier in a dedicated field such as:\r\n\r\n```text\r\nentra_sub\r\n```\r\n\r\nThe authentication flow should resemble:\r\n\r\n```php\r\n$sub = (string) (\r\n    $this->token->oid\r\n    ?? $this->token->sub\r\n    ?? ''\r\n);\r\n\r\n$userId = $this->userRepo->findUserIdByEntraSubject($sub);\r\n\r\nif ($userId === null) {\r\n    $userId = $this->provisionNewSsoUser($sub, $claims);\r\n}\r\n\r\n$user = CurrentUser::getById($userId);\r\n```\r\n\r\nThe implementation should **never automatically link an external Entra identity to an existing local account based solely on username or email**.\r\n\r\nIf account linking is required, it should require an explicit authenticated account-linking workflow where the existing local user confirms ownership.\r\n\r\nAdditionally:\r\n\r\n* Do not automatically call `setStatus('active')` during normal SSO authentication.\r\n* Do not automatically convert an existing account's `auth_source`.\r\n* Do not overwrite the existing account's email address merely because an SSO login occurred.\r\n* Reject identity collisions instead of silently adopting the local account.\r\n* Store the immutable Entra subject when a new SSO account is provisioned.\r\n* Log and alert on external-identity linking operations.\r\n* Ensure local administrator authentication remains independent of SSO identity collisions.\r\n* Validate the OpenID Connect `nonce`.\r\n* Validate OAuth/OIDC `state` during the callback.\r\n\r\nA regression test should verify the following condition:\r\n\r\nGiven:\r\n\r\n```text\r\nlocal username = admin\r\n```\r\n\r\nand a valid Microsoft Entra ID token containing:\r\n\r\n```text\r\npreferred_username = admin\r\noid = previously-unlinked-identity\r\n```\r\n\r\nthe request must:\r\n\r\n```text\r\nNOT authenticate as admin\r\nNOT activate admin\r\nNOT change admin.auth_source\r\nNOT change admin.email\r\n```\r\n\r\nThe application should instead reject the identity collision or provision a separate explicitly linked SSO account.\r\n\r\nReferences :\r\n\r\nAffected source code:\r\n\r\n* `phpmyfaq/src/phpMyFAQ/Auth/AuthEntraId.php`\r\n* `phpmyfaq/src/phpMyFAQ/Auth/EntraId/OAuth.php`\r\n* `phpmyfaq/src/phpMyFAQ/Controller/Frontend/AzureAuthenticationController.php`\r\n* `phpmyfaq/src/phpMyFAQ/User.php`\r\n\r\nRelevant reviewed locations:\r\n\r\n* `AuthEntraId.php:63-84`\r\n* `AuthEntraId.php:69-81`\r\n* `OAuth.php:233-236`\r\n* `AzureAuthenticationController.php:122-135`\r\n* `User::createUser()`\r\n* `User::getUserByLogin()`\r\n* `User::extractUserFromResult()`\r\n\r\nWeakness classifications:\r\n\r\n* CWE-287: Improper Authentication\r\n* CWE-290: Authentication Bypass by Spoofing\r\n* CWE-863: Incorrect Authorization\r\n\r\nAuthentication standard considerations:\r\n\r\n* OpenID Connect immutable subject identifiers (`sub`)\r\n* Microsoft Entra immutable identity identifiers (`oid`)\r\n* Microsoft identity platform ID-token claim guidance regarding `preferred_username`\r\n\r\nRelated implementation comparison:\r\n\r\nphpMyFAQ'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.\r\n","categories":["Unclassified"],"product":"phpMyFAQ","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-6hhv-gc4g-f4r7","source_modified_at":"2026-10-03T11:33:22Z","fetched_at":"2026-10-24T10:48:42.330819Z","package":{"ecosystem":"php","name":"phpMyFAQ"},"range":"<= 4.2.0-alpha.2"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-6hhv-gc4g-f4r7","source_modified_at":"2026-10-03T11:33:22Z","fetched_at":"2026-10-24T10:48:42.330819Z","package":{"ecosystem":"php","name":"phpMyFAQ"},"version":"4.2.0-beta"}],"cvss":7.4,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-03T11:33:22Z","modified_at":"2026-10-03T11:33:22Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-6hhv-gc4g-f4r7","fetched_at":"2026-10-24T10:48:42.330819Z","source_modified_at":"2026-10-03T11:33:22Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-03T11:33:22Z","content_hash":"336809b2a9f37e3cc064e9c7c6d19b2b65b2dc8bd1e91c9749727f67451dbdef"}}
