A forged Firebase Phone Auth token signed with the attacker’s own RSA key impersonates any phone number on the store; the fix landed in 4.21.0, as one security line inside a 25-item feature release.
What happened
NVD published CVE-2026-13447 at 2026-09-05T06:17:09.403, assigned and scored by [email protected] at CVSS 9.8 with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, filed under CWE-287. The record is still Received, so the CNA’s own description is currently the whole of it. It reads:
“The Mstore Api plugin for WordPress is vulnerable to Authentication Bypass via JWT Forgery in versions up to, and including, 4.20.0 This is due to missing cryptographic signature verification in the FirebasePhoneAuthHelper::verify_id_token() function, which decodes and validates Firebase ID token claims (alg, kid, aud, iss) but never calls openssl_verify() or any equivalent to validate the JWT signature against Google’s actual public key certificates. This makes it possible for unauthenticated attackers to forge a Firebase Phone Auth JWT signed with a self-generated RSA key pair and impersonate any phone number, resulting in unauthorized access to existing WordPress accounts or creation of new arbitrary accounts.”
Mstore API is the WordPress-side bridge for FluxBuilder, an app builder that turns a WooCommerce store into native Android and iOS apps. It exposes the REST endpoints those apps call, and Firebase Phone Auth is one sign-in route it offers: the shopper gets an SMS code, Firebase issues an ID token, and the app hands that token to the store to be exchanged for a WordPress session.
WordPress.org reports 3,000+ active installations and lists 4.21.3 as current, last updated about two weeks before this was written. The vendor’s changelog places the fix in 4.21.0, in an entry that reads “Security: Fix Firebase JWT signature, audience validation, and key caching,” alongside roughly twenty-five other items covering Listeo listing fields, booking options, push notifications, product fields, and vendor registration.
What the guard did check
The function is not empty. Per the record it decodes the token and validates four things: alg, kid, aud, and iss. Those are the header and payload fields that describe the signature and its issuer. It confirms the token says it was signed with the expected algorithm, names a key ID, is addressed to this Firebase project, and claims to come from Google.
Then it never calls openssl_verify(), or anything equivalent, against Google’s public certificates. Every one of those four checks is a check on what the token asserts about itself, and the token is attacker-supplied in full. An attacker generates an RSA key pair and signs a token whose header says RS256 with a plausible kid, whose aud is the store’s Firebase project ID and whose iss is Google’s issuer string, with the phone number claim set to the victim’s. All four guards pass. The one operation that would have failed — checking the signature against a key the store trusts — is the one that is absent.
Why it matters
The alg check is what makes this worth writing up rather than filing as another missing-validation bug.
Validating alg is the standard remediation for the best-known JWT vulnerability there is: the alg: none attack, and its cousin where a token declaring HS256 is verified with an RSA public key as the HMAC secret. Every JWT hardening guide says to pin the expected algorithm and reject anything else. This code did that. Somebody read the guidance, understood the famous failure mode, and implemented the defense against it — and shipped a verifier that does not verify. That the sophisticated check is present is evidence the developer was thinking about JWT security, which is why nothing about the function looks wrong on review.
The kid check compounds it. A key ID is only useful if you are about to select a key and use it. Checking that a kid is present and well-formed, and then not fetching or applying the key it names, is a control that has assembled every part of the operation except the operation. The vendor’s own fix line — “Fix Firebase JWT signature, audience validation, and key caching” — suggests key handling was reworked at the same time, which fits: the caching machinery was there, and the cached keys were not being used to verify anything.
This is the fifth instance in a week of a control that reads correctly in English and tests something adjacent to the property that matters. Hummingbird’s debug-log guard called class_exists() on the wrong namespace, so it never matched in any version. python-jose checked a key’s armor strings instead of its type. Kestra’s auth filter called endsWith on a path instead of asking whether the route was auth-exempt. goose’s recipe scanner scanned every field except the two that execute commands. The common defect is not carelessness. It is that each check tests a proxy for the property, and the proxy is cheap to satisfy.
What separates this one from the other four is the identity it forges. The others produce code execution or a bypassed route. This produces a session as a specific named person, chosen by the attacker, on a storefront. A phone number is the account key here, and phone numbers are public: they appear on invoices, in order confirmations, and in the store’s own customer records. There is no discovery step. An attacker who wants the account belonging to a particular customer, or to the store owner if the owner signed in through the app, needs only that number.
The account-creation half deserves separate weight. The record says the flaw permits “creation of new arbitrary accounts,” which means an attacker is not limited to taking over an existing customer. They can mint WordPress users at will against a store whose registration policy may otherwise be closed. On a WooCommerce site, a customer account carries order history, saved addresses, stored payment references, and in many configurations store credit or loyalty balances — and the vendor’s own 4.21.2 release, two versions after the fix, adds “Security: Require authentication and ownership on the loyalty points endpoints,” which indicates loyalty balances are reachable through this same API surface.
On scale, be careful in both directions. WordPress.org’s 3,000+ band counts installs of the free plugin from the directory. Mstore API is the backend for a commercially sold app builder, so an unknown number of deployments arrive through purchase rather than the directory and are invisible to that count. Nobody publishes the real figure. The honest statement is that the directory reports 3,000+ and the true population is larger by an unknown factor.
What to do
- Update to 4.21.0 at minimum; 4.21.3 is current. The Firebase JWT signature fix is in 4.21.0 per the vendor changelog, and 4.21.1 and 4.21.2 each carry further security entries on adjacent endpoints — order-completion and wallet bypasses in 4.21.1, loyalty-point authorization and check-user enumeration throttling in 4.21.2. Going to 4.21.0 and stopping leaves those.
- Check whether phone sign-in is actually in use. The record states no precondition, and it could not be established from here whether the Firebase Phone Auth route is reachable on an installation that never configured it. Treat it as reachable until you have confirmed otherwise in your own configuration.
- Audit accounts, not just logins. Because a forged token produces an ordinary authenticated session, there is nothing anomalous in the access log to search for. The check that works is inventory: list users created or modified since you deployed a version at or below 4.20.0, look for accounts whose phone number matches an existing customer, and look for registrations that should not have been possible under your registration policy.
- Review balances and stored value on any store using the loyalty or wallet features, given what 4.21.1 and 4.21.2 fixed on that same API surface.
- For anyone writing token verification: assert the outcome, not the mechanism. A test that feeds the verifier a syntactically perfect token signed with a key the server has never seen, and requires rejection, fails on day one against this function. A test that checks “does the verifier validate
alg” passes.
Sourcing note
The CVE record was read from NVD’s API and its description is reproduced above complete and unedited, including the missing period after “4.20.0,” which is in the record as published. The record is vulnStatus: Received: no NVD enrichment, no CPE configuration, no independent vector. No CISA KEV fields are present — this is not a KEV entry and carries no federal deadline.
Version, install count, and changelog text come from the plugin’s WordPress.org directory page, read directly. One caveat on the 4.21.0 quotation: two retrievals of that page returned the release differently, one itemizing the security line quoted above and one describing 4.21.0 only as roughly twenty-five improvements “including Firebase JWT validation.” Both place the Firebase JWT signature fix in 4.21.0, but the exact wording of that line rests on a single retrieval. The 4.21.1, 4.21.2 and 4.21.3 entries were itemized identically on both.
Wordfence’s own advisory page for this CVE returned no usable content, as its threat-intel pages have on every attempt this site has made; researcher credit and Wordfence’s disclosure timeline are therefore unavailable. The Trac line references in the record — flutter-user.php lines 829 and 940, and firebase-phone-auth-helper.php line 5, at tag 4.18.4 and on trunk — could not be opened, because plugins.trac.wordpress.org/browser/ URLs are disallowed to automated retrieval. The description of what the function does and does not check is therefore the CNA’s, not an independent reading of the source.
Unresolved: the disclosure date and the time between report and fix. The fix is in 4.21.0 and the CVE published after 4.21.3 shipped, so the record trailed the patch by at least three releases, but by how long cannot be stated from the sources reachable here.

