{"schema":"threatline.record/2","record":{"id":"GHSA-43g3-pp5p-fg96","aliases":["GHSA-43g3-pp5p-fg96"],"summary":"SSO login (Keycloak and Microsoft Entra ID) bypasses TOTP two-factor authentication","details":"## Summary\nphpMyFAQ's SSO login flows (Keycloak and Microsoft Entra ID / Azure AD) grant a fully authenticated session immediately after the identity provider validates the user, without checking whether the linked account has TOTP two-factor authentication enabled. Any user with a valid IdP identity can log in as the corresponding phpMyFAQ account bypassing the second factor, even when 2FA is configured.\n\nThis is a different code path from CVE-2026-56737 (GHSA-8gpw-xvpf-hvx5), which covered the public `POST /check` password-factor bypass. Here the affected endpoints are the OAuth/OIDC callbacks, and 2FA is skipped entirely on successful SSO — not just the password step.\n\n## Root Cause\nBoth SSO callback controllers call `$user->setLoggedIn(true) / updateSessionId(true) / saveToSession() / setSuccess(true)` directly after successful IdP validation, without inspecting the account's `twofactor_enabled` user-data flag or deferring to the token step. The password and passkey login flows correctly defer via `2fa_pending_user_id` — the SSO flows omit this.\n\nAffected files:\n- `phpmyfaq/services/azure/callback.php` (legacy Azure/Entra ID handler, present in 4.1.x)\n- `phpmyfaq/src/phpMyFAQ/Controller/Frontend/AzureAuthenticationController.php` (4.2.0-alpha)\n- `phpmyfaq/src/phpMyFAQ/Controller/Frontend/KeycloakAuthenticationController.php` (4.2.0-alpha)\n\n## Impact\nComplete bypass of the second authentication factor for every 2FA-enabled account reachable via SSO. In enterprise deployments where phpMyFAQ is federated with Keycloak or Entra ID, any authenticated IdP user who is also mapped to a phpMyFAQ admin/user account (including users who set up 2FA specifically to protect that account) is logged in without ever presenting the TOTP token. This defeats the entire purpose of enabling TOTP on SSO-linked accounts and lets a compromised IdP session or phished IdP credential produce full phpMyFAQ account takeover with no TOTP knowledge.\n\nCWE-287 (Improper Authentication) / CWE-306 (Missing Authentication for Critical Function).\n\n## Attack Chain\n\n1. **Entry:** Attacker (holding a valid IdP identity — e.g. an ordinary Keycloak/Entra user in the org that is federated to phpMyFAQ) navigates to `/auth/keycloak` or `/services/azure/index.php` on the target phpMyFAQ instance.\n   - **Guard:** none — public OAuth initiation endpoint.\n   - **Bypass proof:** endpoint is designed to accept any visitor to start the OIDC dance.\n\n2. **IdP round-trip:** IdP authenticates the attacker and redirects back to the callback with an authorization code.\n   - **Guard:** IdP-level authentication (attacker must own the IdP identity).\n   - **Bypass proof:** the attacker holds a legitimate IdP account — this is not the barrier being bypassed. The barrier being bypassed is the *second factor on the phpMyFAQ side*.\n\n3. **Callback processing (Keycloak):** `KeycloakAuthenticationController::callback` exchanges the code, loads the linked phpMyFAQ user, and executes:\n   ```php\n   $user->setLoggedIn(true);\n   $user->setAuthSource(AuthenticationSourceType::AUTH_KEYCLOAK->value);\n   $user->updateSessionId(true);\n   $user->saveToSession();\n   // ...\n   $user->setSuccess(true);\n   ```\n   - **Guard:** `$user->getUserData('twofactor_enabled')` is available but never consulted. No `2fa_pending_user_id` deferral like the password/passkey flows perform.\n   - **Bypass proof:** fix commit `ebca8fd1` (2026-09-27) adds exactly the missing `if ((int) $user->getUserData('twofactor_enabled') === 1)` gate and moves `setLoggedIn/updateSessionId/saveToSession/setSuccess` inside the else branch — the diff itself is the proof the gate was absent.\n\n4. **Callback processing (Entra ID / Azure):** identical pattern in both the legacy `phpmyfaq/services/azure/callback.php` (4.1.x, `setLoggedIn(true)` at line 72, `setSuccess(true)` at line 82) and the 4.2 controller `AzureAuthenticationController.php` (`setLoggedIn(true)` at line 132).\n   - **Guard:** none — same omission as Keycloak.\n   - **Bypass proof:** same fix commit `ebca8fd1` plus its precursor `e57e9a03` add the `twofactor_enabled` check to both files.\n\n5. **Sink:** Session is now authenticated as the target phpMyFAQ user; no token step is ever presented to the browser.\n   - **Impact:** full account takeover for any 2FA-enabled account reachable through SSO — including administrators.\n\n## Bypass Evidence\n\n- `git show 4.1.8:phpmyfaq/services/azure/callback.php` shows `setLoggedIn(true)` at line 72 and `setSuccess(true)` at line 82 with no reference to `twofactor` anywhere in the file: `git show 4.1.8:phpmyfaq/services/azure/callback.php | grep -n twofactor` → (empty).\n- `git show 4.2.0-alpha.2:phpmyfaq/src/phpMyFAQ/Controller/Frontend/KeycloakAuthenticationController.php | grep -n twofactor` → (empty); `setLoggedIn(true)` present at line 233.\n- `git show 4.2.0-alpha.2:phpmyfaq/src/phpMyFAQ/Controller/Frontend/AzureAuthenticationController.php | grep -n twofactor` → (empty); `setLoggedIn(true)` present at line 132.\n- Fix commits `ebca8fd1` and `e57e9a03` on branch `main` (2026-09-27) add the `getUserData('twofactor_enabled') === 1` deferral to the `token?user-id=` step in all three files. `git merge-base --is-ancestor ebca8fd1 4.2.0-alpha.2` returns non-zero — the fix is NOT in the latest release.\n\n## Proof of Concept\n\nPreconditions: phpMyFAQ is configured with either Keycloak or Entra ID SSO; a target user account (e.g. an admin) has enabled TOTP two-factor authentication; the attacker holds any IdP identity that is linked (by email / username / preferred_username claim) to that phpMyFAQ account (a common outcome of federated deployments).\n\n1. Attacker opens `https://target/auth/keycloak` (or the Entra initiation endpoint).\n2. Attacker authenticates to Keycloak/Entra with their own valid IdP credentials.\n3. IdP redirects back to the phpMyFAQ callback with an authorization code.\n4. phpMyFAQ exchanges the code, loads the linked account, sets `LoggedIn=true`, writes the session, redirects to `./`.\n5. The attacker now holds an authenticated session for the phpMyFAQ account **without ever seeing the TOTP token page**.\n\nResponse on the successful callback is a `302` to `./` with a `Set-Cookie: PHPSESSID=...` binding a fully logged-in session — no TOTP challenge issued.\n\n## Affected Versions\n- `phpmyfaq/services/azure/callback.php` (Entra ID): `<= 4.1.8` (and all 4.1.x releases) and `>= 4.2.0-alpha, <= 4.2.0-alpha.2` via the new controller.\n- `KeycloakAuthenticationController.php`: `>= 4.2.0-alpha, <= 4.2.0-alpha.2` (Keycloak SSO was introduced in the 4.2 alpha stream; not present in 4.1.x).\n\nFix is committed on `main` in `ebca8fd1` / `e57e9a03` (2026-09-27) but is not yet included in any tagged release.\n\n## Suggested Fix\nAlready implemented on `main` — see commit `ebca8fd1`. Pattern: after IdP validation and before `setLoggedIn(true)`, check `(int) $user->getUserData('twofactor_enabled') === 1`; if true, verify `isTwoFactorLockedOut()`, set `2fa_pending_user_id` / `2fa_pending_remember_me` in the session, and redirect to `token?user-id=<id>` instead of finalizing the login. This mirrors the password and passkey flows and closes the SSO gap. Backport to 4.1.x for the legacy `phpmyfaq/services/azure/callback.php`.\n\n\n---\nReported by **zx (Jace)** — GitHub: [@manus-use](https://github.com/manus-use)","categories":["Unclassified"],"product":"phpmyfaq/phpmyfaq","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-43g3-pp5p-fg96","source_modified_at":"2026-10-03T11:26:31Z","fetched_at":"2026-10-24T14:26:37.433577Z","package":{"ecosystem":"composer","name":"phpmyfaq/phpmyfaq"},"range":"<= 4.2.0-alpha.2"}],"fixed_versions":[],"cvss":8.1,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-03T11:26:31Z","modified_at":"2026-10-03T11:26:31Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-43g3-pp5p-fg96","fetched_at":"2026-10-24T14:26:37.433577Z","source_modified_at":"2026-10-03T11:26:31Z"}],"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:26:31Z","content_hash":"16660b898eecff5de5b697ea2e4621b8c2bcc5f638234f9ba2a399da44b653f2"}}
