WordPress plugin · High, 8.6 of 10 · no patch
The verification code you never needed
A plugin that emails one-time codes to confirm an address checked those codes with a SQL query. It built the query by pasting the request’s values straight into the text, then handed the finished string to WordPress’s prepare(), which can only protect values it is given separately. There were none left to protect. In our lab, one anonymous request made the check answer yes without the code. It is public as CVE-2026-89299, scored 8.6 of 10 by WPScan, and credited to RIA Labs.
Read this part first
If your site uses a plugin that emails one-time codes to verify an address, read the advisory. It names the plugin and the affected versions. As of 9 October 2026 there was no patch, and the plugin’s wordpress.org listing had been closed since 17 September 2026, pending a full review. A closed listing does not remove the plugin from a site that already has it. Deactivate it; there is no update to wait for.
If your site used it to verify addresses, treat every address verified through it as unverified, and check them again some other way.
The rest of this is about the pattern. There is no exploit code below, and none of the names that would make one easy to write. On 9 November 2026 WPScan plans to show the proof of concept we sent them. It will never appear here.
How bad it is
8.6 of 10, WPScan’s score: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N. Anyone on the internet, no account, no click. The endpoint answers one question, “is this the right code for this address?”, and in our lab the injection made it answer yes. Once that is possible, “verified” stops meaning the address belongs to the person who claimed it.
WPScan’s vector scores what an injection like this could read: confidentiality high, integrity none. What we ran was the bypass, and we read nothing. Our report filed this with the plugin’s other bug and scored the pair 9.1, counting the bypass as an integrity hit. WPScan split them and scored this half 8.6. Theirs is the number of record.
The other half is the plugin’s verification email anyone could send to anyone, CVE-2026-89300, scored 5.3 by WPScan. Both endpoints had to be public. Neither treated what it was sent as coming from a stranger.
prepare() was there. It came too late.
Our last post asked whether every query uses a prepared statement. This plugin would have answered yes. WordPress gives plugins a function for building safe queries. You write the query with placeholders, pass the values separately, and the function escapes each value as it fills them in. The safety comes from keeping the values apart from the text until that moment.
This plugin joined the values into the query text first and then passed the finished string through the same function. By then there was nothing to escape: the values were already part of the query. The call looks careful at a glance and protects nothing. A search for queries that skip prepare() walks right past it. WordPress does not: in debug mode it logs a “doing it wrong” notice when prepare() gets no placeholders, and the WordPress coding-standards linter flags values pasted into a query. The warning existed. It had to be read.
Worse than it first looked
The first pass read this as a blind injection: slow, one bit at a time, worth about 7.5. A second, adversarial pass re-read it before it went out, and found the injection could make the check itself say yes. The report went out at 9.1 because of it. Since then our workflow gives that pass two jobs: try to prove it false, and try to show why it is worse than it looks.
How we know
A description is not a finding. The engine built a disposable WordPress site with the plugin’s official 1.0.0 release, verified by hash, and turned on the database’s query log. An invented code was rejected: the control. A crafted request then ran its SQL exactly as written, the log shows it, and the endpoint answered yes with no knowledge of the code. No one else’s site was touched.
The first run failed for a reason unrelated to the bug, a timing problem in the test setup. The query log showed it, and the second run worked. Before reporting, we checked four vulnerability databases and found no earlier record for this plugin.
Three questions for your own plugins
None of this is specific to one plugin. Any code that checks a secret against a database is worth asking these about.
- Does
prepare()get placeholders, or a finished string? If any value is in the query text before the call, the call is decoration. - Does the check treat its input as hostile? Code anyone can call should be the strictest on the site, not the most trusting.
- What does your site do with “verified”? Whatever it unlocks, a bypass of the check unlocks too. That is the real blast radius, and it is yours to measure.
None of it needs exotic tooling. It needs a linter WordPress developers can already run, or someone who reads where the values enter the query.
What it proves, and what it doesn’t
It proves a second, adversarial look is worth running: this finding left our pipeline more serious than it entered.
It does not prove we read data out of a database. The bypass is what we ran; extraction was not attempted. It shows the bypass on one fresh lab install. It does not show the same request works unchanged on every live site. It does not say what any site used the verification for, and it does not say how many still run the plugin: wordpress.org listed about ten installs when we tested in August, the plugin had not been updated since 2020, and the listing is now closed.
If you ship a plugin, or run a site full of them, we’ll read it. Nominate a target and the first scan costs nothing.
Published 9 October 2026. CVE-2026-89299 · CVSS 3.1 base score 8.6 · CWE-89 (SQL Injection) · credited to RIA Labs, scored and published by WPScan. Status as of 9 October 2026. Corrections, or a plugin you maintain and want looked at: info@ria-labs.com.
See what a scan turns up
Nominate a target. The first scan costs nothing, and we’ll walk you through it.