← Crucible

The next one paid $135

· disclosure bug-bounty patch-review

Wordfence published my second CVE with them today. It paid $135, and that's the first money this project has ever earned.

CVEPluginClassFixed in
CVE-2026-19985RelevanssiUnauthenticated reflected cross-site scripting · CVSS 6.14.28.2

I reported it on the sixth of August. The identifier came ten days later, the money on the first of September, and the advisory today. Nothing appears here until Wordfence says disclosure is finished, so these posts run on a deliberate delay.

The money landed nine days before the advisory did. A payment notification says nothing about whether you're allowed to talk, so I waited for both.

What the bug was

Relevanssi is a search plugin. It has a debugging mode an administrator can switch on. With it on, a request can ask a page that ran a search to dump the internals of that search.

The dump was written straight into the page, with nothing escaping it on the way out. Some of what it prints comes off the request.

The gate on the whole thing is a check that a setting is enabled. Not a check on who's asking. So on a site with debugging left on, an unauthenticated visitor could get script into the page, and the victim is whoever opens the link.

That precondition is real and I'm not going to dress it up. This needs an administrator to have turned debugging on and left it on, and it's off by default.

The advisory doesn't actually say that last part. I did. It records that an administrator must have enabled the setting, which isn't the same statement as how it ships. The default is off, and it's my report that cites the install line setting it, not theirs.

What the score doesn't do is reflect any of this. 6.1 with low attack complexity is the ordinary reflected cross-site scripting vector. Nothing in it encodes "an administrator had to switch something on first". Wordfence's own record for an almost identical bug in another plugin, a reflected flaw reachable only through its debug mode, carries the same 6.1 and the same vector. When I measured their published set, better than nine in ten reflected records carried it. It's the house shape, not a reading of my particular bug. Preconditions live in the prose. They don't live in the number, and I'd been quietly hoping otherwise.

Why this one paid

Five days ago I wrote about my first CVE, which paid nothing. Wordfence validated that finding and then judged its impact into a lower category than the one I'd filed it under, and the category is what the money hangs on. That post also refused the tidy version of the story, and I'll keep refusing it here: correct classification wouldn't have paid either, because the plugin was too small.

Out of that came a habit of settling two numbers before doing any work. How many installs the plugin has, and the minimum that the impact I can actually prove requires.

Two more reports went to Wordfence between that ruling and this one. Both cleared those two numbers comfortably. Both came back out of scope anyway, on a question the numbers don't measure. So the two numbers are necessary rather than sufficient. This is the first report that got past scope and reached bounty review, and the first that got paid.

For this one the numbers were boring in the right way. Reflected cross-site scripting sits in the band that needs 50,000 installs at my tier. Relevanssi has about 100,000.

I'd rather not claim that nothing had to be decided. That isn't what happened. My own notes carry a table weighing four possible classifications and rejecting three. Two of the three would have paid considerably more if I could have supported them. I couldn't, so I filed the one I could. That table is the work. Skipping it is how you end up in a bounty review explaining a category your evidence doesn't reach.

The advisory settled one thing I'd been unsure of. The published vector is character for character the one I submitted, so Wordfence didn't rescore the severity. They took the finding at the weight I gave it, so whatever set the figure, it wasn't the score. The category is the lever.

Reading the fix

The advisory says 4.28.2 fixes it, which is the vendor's claim until I open the release. So I opened it.

The fix is one line. The print call now hands its output back as a string instead of writing it out, and that string is escaped before it reaches the page.

What makes it the right line is where it sits. It's at the sink rather than at any of the inputs. All three parameters the advisory names converge on one array, dumped by a single call. That same function has three other call sites as well, carrying the processed query, the tokenised search terms, and a post's entire meta table. Escaping inside the function closes all four at once. Fixing the inputs would have given the vendor several chances to miss one. It would also have done nothing for the call site that never touches a request.

I grepped the rest of the release too. Across all 107 PHP files in 4.28.3 the raw print call appears exactly once, and it's inside the line that was fixed. That's a narrow check and I don't want it to sound like more. The same debug file still has other unescaped output. I read that as safe, because what it prints comes out of the database and the settings table rather than off the request.

The next release, 4.28.3, shipped the same day the advisory published and carries that file byte for byte unchanged. It does add new debugging output elsewhere in the admin area, which I read and did not test.

I read the fix. I didn't stand up a patched install and fire the original request at it, which is the test that would actually settle it. Those are different strengths of evidence and I'd rather say which one I've got than blur them. I still don't know where that line should sit. Reading is cheap and I do it every time. Standing the plugin up costs real hours, and so far I've only ever spent those on the original finding, never on the fix.

If you run Relevanssi, update to at least 4.28.2. Then, separately: if you switched debugging on to diagnose something last year, go and check whether you switched it back off. The update stops the script injection. It doesn't make a debug view something you want facing the public, because escaping what a diagnostic prints doesn't change what it's willing to print.

There's no proof of concept here, no script and no request shapes, and there won't be one later. Wordfence doesn't publish one either. I don't publish them at all.


This work uses large language models throughout the toolchain, including in drafting this post. Every finding is checked by hand against the source before anything is submitted. The finding was reported through the Wordfence bug bounty programme, who coordinated disclosure with the vendor and assigned the identifier. The fix release was read directly from the vendor's published source.