WP Manifestindependent plugin directory
manifest / security / two-factor-webauthn

Two Factor Web Authentication WP Registry grade C-. High-severity findings · 1 findingWP RegistryHigh-severity findings · 1 findingOpen the reportC-

WebAuthn for the Wordpress Two Factor plugin

★ 4stars
17release downloads
≈10active sites
0.1.6latest · 11mo ago

Install

Download two-factor-webauthn.zip From the 0.1.6 release on GitHub, Oct 29, 2025

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/mcguffin/two-factor-webauthn/releases/download/0.1.6/two-factor-webauthn.zip
Updates itself. It ships its own updater (Update URI header), so new versions appear under Dashboard → Updates.

WebAuthn module for the Two Factor WordPress plugin.

Incorporates a modified version of WebAuthn by David Earl.

Current Status

This plugin is currently in beta stadium. I still hope to have it merged into Two Factor later, but the response doesn't look very promising yet: https://github.com/WordPress/two-factor/issues/232

Installation

  • Head over to releases
  • Download 'two-factor-webauthn.zip'
  • Install and activate it like any other WordPress plugin
  • As long as the plugin is active, it will check for updates

Development

cd wp-content/plugins
git clone git@github.com:mcguffin/two-factor-webauthn.git
cd two-factor-webauthn
npm install
npm run dev

Releases

2 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
0.1.6 latest
Oct 29, 2025 11mo ago
two-factor-webauthn.zip
11
Nov 29, 2024 1 year ago
two-factor-webauthn.zip
6

Active-site estimate ≈10 comes from the median of recent superseded releases. Method.

C- grade

Security

Two Factor Web Authentication 0.1.6 · audited by WP Registry

High-severity findings.

1 high
Audited release
0.1.6
Findings
1
Worst severity
high
Content hash
2ec6bce3340aaba459ca46ac…

Findings

  • high WebAuthn 2FA assertion replay — challenge never stored server-side, sign counter ignored

    auth_bypass

    Two_Factor_Webauthn::authentication_page() generates a 16-byte challenge via prepareAuthenticate() and sends it to the browser (wp_localize_script webauthnL10n.payload) but never persists it. The only leftover server flag, usermeta `_two_factor_webauthn_login`, is set to the literal 1 and is never read in validate_authentication(). On the way back, WebAuthn::validateAuthenticateInfo() treats the client-supplied originalChallenge as the expected challenge and only checks that it equals clientData.challenge — also client-supplied. Those two fields are not required to match the signed clientDataJSONarray that openssl_verify() actually covers, so the origin/type/challenge checks are theater. authenticate() then copies the 4-byte authenticator counter out of authenticatorData and never compares it to a stored signCount, so a cloned or replayed assertion is indistinguishable from a fresh one. Upstream davidearl/webauthn added pass-by-reference challenge storage and one-time clearing in December 2022 specifically 'to prevent a possible replay of a login'; this plugin's vendored copy never received that fix. Combined with stolen or phished primary credentials (the exact case 2FA exists for), one captured webauthn_response POST body logs the attacker in as the victim on every subsequent login, with no expiry. Registration (validateRegisterInfo) likewise never checks clientData, origin, or challenge, so a logged-in user can enroll a software-generated public key by POSTing a crafted attestationObject; that is secondary to the login replay.

    include/Davidearl/WebAuthn/WebAuthn.php:214-300

    Recommendation

    Store the challenge server-side (user meta or a single-use transient keyed to the Two Factor login nonce), bind it to the user id, and consume it on successful or failed authenticate() so it cannot be reused. Compare clientDataJSON bytes (the signed payload) to the parsed clientData used for origin/type/challenge checks. Require origin to be an exact allowlisted origin (scheme + host + port), not an unbounded host suffix. Persist authenticator signCount per credential and reject assertions whose counter is not greater than the stored value. Port the December 2022 davidearl/webauthn replay fix or replace the library with a maintained WebAuthn server (e.g. lbuchs/WebAuthn / web-auth/webauthn-lib) that implements the spec verification steps.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.