Skip to content

phpMyFAQ coordinated wave: unauthenticated WebAuthn fmt=none passkey mint, mutable-claim IdP linking, readable TOTP secrets, and scope-dropping privilege APIs

Scan date: 2026-10-17 22:1xZ UTC · Vendor band: phpMyFAQ before 4.1.9 and 4.2.0-alpha before 4.2.0-beta · Wave: ~28 GHSAs published in one 21:31Z batch (CVE-2026-111880-adjacent range 111882–111911 minus non-phpMyFAQ IDs)

This is phpMyFAQ's largest coordinated dump (previous wave: May 31, account-takeover boundary page). The advisories are unusually descriptive — each one names the function, the missing check, and the exact request shape, which makes the batch a copy-pasteable audit checklist for any PHP FAQ/portal/CMS with delegated-admin models. The crown-jewel legs all sit on the identity lifecycle: passkey enrollment, IdP account linking, 2FA state, and admin-scope APIs.

Advisory table (legs with durable operator value)

GHSA CVE Leg
GHSA-768h-xwv2-fwp3 CVE-2026-111911 WebAuthn registration endpoints accept unauthenticated requests, skip challenge validation, and accept forged fmt=none attestations — stage a victim username, submit a fmt=none attestation, a passkey is attached to the victim's account, then log in with the passkey bypassing password AND TOTP = full ATO
GHSA-r5w3-grm8-fh38 CVE-2026-111895 Entra ID login links accounts by the mutable preferred_username claim and AuthEntraId::create() swallows the duplicate-login exception and continues with the existing local account — attacker controls an Entra identity in the configured tenant with a matching name → takes over that local account, including super-admin
GHSA-8rmr-jc2p-xv7g CVE-2026-111898 Same shape via Keycloak: links by preferred_username or unverified email → bind attacker subject → superadmin privileges
GHSA-74rq-7w8c-gr94 CVE-2026-111882 Keycloak/Entra SSO callbacks skip TOTP entirely — a valid or phished IdP identity linked to a 2FA-enabled account yields a fully authenticated session; second factor enforced only on the local password path
GHSA-ch92-qpmj-fc56 CVE-2026-111884 Account email change (UserController::updateData) checks only session + CSRF — no current-password step-up; hijack a session, set email to yours, run password-reset → ATO of non-2FA accounts incl. superadmin
GHSA-44gp-7cgr-6p92 CVE-2026-111905 GET /admin/api/user/data/{userId} returns the full faquserdata row including the live TOTP secret with no SuperAdmin or protected-target check — delegated user-management admins read a SuperAdmin's seed and generate valid codes
GHSA-9p2j-4h8p-3832 CVE-2026-111886 POST /api/user/remove-twofactor has no brute-force lockout on the TOTP confirmation — hijacked session + CSRF token allows unlimited guesses to permanently strip 2FA
GHSA-vj43-979m-cpx8 CVE-2026-111891 Group permission API (updatePermissions/updateMembers) checks held rights without regard to language/category scope and writes unrestricted grants — narrow-scope admin self-joins a group and gains global FAQ create/edit/publish
GHSA-x3mg-fj9x-7cq2 CVE-2026-111890 UserController::updateUserRights drops per-right language restrictions when rights are re-granted — submit your own scoped rights back and they land unrestricted
GHSA-xr8j-x8f4-2j2f CVE-2026-111893 GroupController::saveCategoryRestrictions() lets category-restricted admins POST out-of-scope or empty categoryIds to grant groups unrestricted category access
GHSA-346r-939v-qh8c CVE-2026-111902 isFaqAccessibleForUser() treats a missing FAQ translation as visible — send a mismatched Accept-Language to comments/attachments endpoints to read ACL-restricted/unpublished content (absent-record fail-open)
GHSA-xmh2-49v9-xw88 CVE-2026-111904 Inverted api.onlyPublicQuestions check in OpenQuestionController — unauthenticated GET /api/v4.0/open-questions returns hidden moderation/internal questions + usernames
GHSA-x6rh-6v24-8xcj / GHSA-qwjg-39wm-p2qc CVE-2026-111910 / 111900 attachment.php checks the caller's dlattachment right but never the parent FAQ's visibility/publication state — iterate attachment IDs to pull files from restricted and draft FAQs
GHSA-cwqc-qp2c-47mg CVE-2026-111908 PUT /api/v3.1/faq/update checks only the global edit_faq permission, no per-FAQ access — edit any restricted FAQ by ID
GHSA-j2g3-mcv7-r3f2 CVE-2026-111888 PUT /admin/api/news/update checks NEWS_DELETE instead of NEWS_EDIT — hold addnews+delnews and you rewrite any front-page news item; the update CSRF token sits in your own cookie
GHSA-c672-4mmj-vq8q CVE-2026-111889 /admin/index.php/ has no authorization gate — any activated account reads unpublished questions, admin edit URLs, recent user identities, site statistics
GHSA-4fcw-x9vq-q4pp CVE-2026-111903 SvgSanitizer bypass via XML DTD custom entities: an entity that expands to javascript: lands in an anchor href after sanitization — script executes in admin sessions on click
GHSA-fch3-7f9r-g5gg CVE-2026-111896 SvgSanitizer single-pass token removal reassembly: split <script> tags around removed processing-instruction tokens so removal rejoins them into live script in the app origin
GHSA-wwmv-7qmx-vxv3 CVE-2026-111894 FaqHelper::cleanUpContent() media-host allowlist matches on host — a hostless data:text/html iframe passes — rendered via Twig raw = stored XSS/phishing in FAQ/News
GHSA-cv2x-hcfv-23pm CVE-2026-111887 Guest comment create returns a distinct HTTP 409 for registered display names/emails — unauthenticated name↔email linking oracle
GHSA-2jrh-wx9p-9fp9 / GHSA-x4f4-6wgq-f686 CVE-2026-111909 / 111885 Delegated admins can delete superadmin accounts (deleteUser() omits protected-target check) and destroy higher-privileged groups (deleteGroup() omits privilege-level check) — teardown legs from a mid-tier admin
GHSA-mm6v-fm3q-x3xw / GHSA-4qjm-33q6-wmgj CVE-2026-111907 / 111906 CSRF (no token validation) on POST /admin/api/update-database migrations and on the Elasticsearch/OpenSearch admin create/drop/import actions

Why this is worth an operator page

Three of the strongest legs are zero-credential account takeover paths, and two of them are grep-able classes that generalize far beyond phpMyFAQ:

  1. fmt=none passkey mint on an unauthenticated registration endpoint. WebAuthn's none attestation format is a legitimate choice for enterprise fleets (no privacy leak, the Relying Party trusts its own enrollment flow). That trust assumption becomes a bypass when the enrollment handler itself has no session gate and no challenge binding: anyone can stage a username and attach their own authenticator. The login path then treats passkey possession as proof of identity — password and TOTP both skipped. Genealogy on the Sept 16 IdP page: ZITADEL authenticator enrollment on identify-only sessions (4727) and enrollment-code org-header drift (hcch) — same rule: authenticator enrollment is a credential write and must require a step-up-verified session for the target account.

  2. Linking on mutable identity claims. preferred_username is editable by the user in both Entra ID and Keycloak; unverified email is attacker-chosen at registration. When an SSO callback links by claim value and the link path swallows the collision exception instead of aborting, controlling an IdP identity with the victim's name IS controlling the victim's local account. This is the GhostPat/convert-flow lineage (identity transitions are credential-carrying state machines) applied to federation: the link decision is a consent question with no consent step. Audit rule for any SSO-enabled product: (a) which claim binds the link, (b) is it mutable by the external subject, (c) is the verified flag actually read (email_verified — Pingvin/Grav/Vikunja legs), (d) what happens on collision — fail-closed or continue-with-existing?

  3. TOTP secret readability by delegated admins. Same class as the Keystone qfjc/90460 delegated-token→/v3/credentials seed read folded Oct 13: a user-management API that serializes the whole row leaks the MFA seed as a side field. Seed read = MFA cloning forever, offline, no victim touch. Pair leg: removeTwofactor unlimited TOTP guessing converts a transient session hijack into a permanent 2FA strip.

Operator rules extracted

  • Fuzz the enrollment/registration endpoints of every passkey/WebAuthn implementation unauthenticated. Three-request check on an authorized target: stage username → submit a fmt=none attestation object → attempt passkey login. Any leg that 200s without a verified session for that username is the bug. Check the server's accepted attestation-format list: none acceptance is fine only behind an authenticated, step-up-gated enrollment gate; challenge binding means the server stores what it issued, not what the client sends.
  • Re-grant paths must re-restrict. Every "update rights/permissions" API that takes a list and writes it wholesale will silently widen scope if the writer normalizes away scope columns (language, category, org). Probe shape: read your own narrow-scope grants with an admin-scoped-GET you legitimately hold, re-POST them verbatim through the update endpoint, re-read. If scope fields vanished or came back unrestricted, every holder of the edit right is one request from global rights. The self-join variant (check-then-write gap between updatePermissions and updateMembers) means you never need another compromised account.
  • Absent records fail open more often than present ones. isFaqAccessibleForUser() returning visible-on-missing-translation and the inverted onlyPublicQuestions boolean are the same shape: the filter's negative branch (no row / flag off / key missing) renders content instead of denying it. Battery: request every "restricted list" endpoint with (a) a header/param spelling that finds no record (wrong Accept-Language, nonexistent category), (b) the config flag in its zero/default value, (c) the filter parameter absent entirely. Compare row counts against a control request.
  • Child-object routes must re-derive the parent's visibility. attachment.php honoring dlattachment while ignoring the parent FAQ's ACL is the parent-binding family (Payload duplicate-copy leg, Gitea attachment URL-repo leg w7q4). Whenever a permission name mentions the child (dlattachment, view_comment, preview), test it against a child whose parent is restricted/draft/unpublished.
  • Verb-gate mismatch inventory: update-checks-delete (phpMyFAQ NEWS legs), delete-checks-edit (Nginx UI family), grant-checks-view. On any RBAC product with distinct add/edit/delete rights per content type, the matrix is small — walk it with an account holding the least-popular right per pair and hit the sibling verb.
  • Single-pass denylist sanitizers are invertible. DTD entity expansion running after sanitization, and split-tag reassembly around removed tokens, are both "the filter sees a different document than the renderer does" — same page as the Sept 20 media-pipeline and Oct 6 Coraza silent-drop lessons. Test SVG uploads with: entity-indirection battery, comment/PI-injected tag splitting, CDATA boundary, and foreign-content (<math>/<svg> <style>) reparsing (SmarterMail MathML leg).
  • Enumeration oracles live in error codes. The 409-on-duplicate-comment and the 403-vs-401 patterns: diff response codes across a known-good/known-absent pair on every endpoint that echoes identity input.

Validation boundaries

  • Prove passkey-mint and IdP-linking legs only in your own disposable phpMyFAQ install with synthetic users, a lab Keycloak/Entra tenant, and attacker-owned authenticators. On live authorized engagements, limit to the unauthenticated decision-table probes (expected-code comparison) and never attach credentials to real accounts.
  • TOTP-seed legs: enroll a lab user, read back the synthetic seed; never touch real users' seeds or generate codes for real accounts.
  • Scope-escalation legs: use a disposable delegated-admin account, snapshot grants before/after, restore state; teardown legs (delete superadmin/group) are lab-only.
  • Keep names, emails, seeds, tokens, and internal URLs out of report exports.

Cross-references

  • Prior phpMyFAQ wave: May 31 account-takeover boundary page (tokenless reset + admin overwrite IDOR — this wave supersedes the model: identity lifecycle is now the attack surface).
  • Enrollment-on-unverified-session + enrollment-code scope drift: Sept 16 IdP authority page.
  • Mutable/unverified-claim linking lineage: GhostPat (Oct 13 page), Pingvin Share email_verified-unchecked (Sept 16 page), Grav derived-federated-credentials (Oct 12 page), Mattermost login/switch (Oct 16 page).
  • Delegated-token → MFA seed read: Keystone leg on the Sept 16 page (Oct 13 fold).
  • Parent-binding and verb-gate drift: Sept 21 auth-precedence page; Oct 9 Nginx UI page (enabled_2fa flag-vs-enforcement).