How a WordPress Log File Turned Into Remote Code Execution
By WP Vanguard Team
On July 6, 2026, the WPFunnels team shipped version 3.12.8 to close CVE-2026-14345, a critical remote code execution vulnerability rooted in how the plugin writes to and reads back its own log file. It carries a CVSS score of 9.8. WPFunnels, sold as "Funnel Builder for WooCommerce with Checkout & One Click Upsell," builds sales funnels, order bumps, and one-click upsell flows on top of WooCommerce stores. Every release up to and including 3.12.7 carries the flaw.
The bug lives in the plugin's logging feature, reached through the 'postData' parameter. It doesn't need a password or a role. It needs two design choices that, on their own, wouldn't hurt anyone: writing raw input straight to a file, and later opening that file with a function meant for code, not data. Put both choices in the same plugin, and an anonymous HTTP request can end with a shell running on the server.
How the flaw works
WPFunnels can log the data each funnel step submits, which is useful when a store owner is trying to figure out why an upsell didn't fire. When logging is switched on, the plugin takes the incoming postData value from a funnel request and writes it into a file with a .log extension, without stripping or encoding anything inside it. If that value contains PHP tags, they land in the log exactly as submitted.
That alone wouldn't be dangerous if the log were only ever read back as text. A .log extension carries no special meaning to PHP. The interpreter doesn't check file extensions before deciding what to run; it executes whatever a script hands it through include, require, include_once, or require_once, regardless of the filename. Rename a PHP payload to notes.log and PHP still runs it the moment something in the codebase calls include_once() on that path.
That's exactly what happens on the read side. The function that renders a saved log for viewing in the admin area, wpfnl_show_log, uses include_once() to pull the file's contents onto the page instead of reading it as plain text. include_once() doesn't display a file. It executes it, the same as any other PHP script the server would run. Once a request has planted PHP inside the log, the next call to wpfnl_show_log doesn't display the log. It runs it.
Neither half of this chain is unusual by itself. Debug logging that captures raw request data is common across WordPress plugins. include_once() is one of the most ordinary functions in PHP. The vulnerability exists only because the two paths cross: the write side never sanitizes what goes in, and the read side never treats what comes out as untrusted.
Versions and conditions
Every site running WPFunnels 3.12.7 or earlier is vulnerable; 3.12.8 carries the fix. Getting from "vulnerable" to "compromised" needs a couple of things to line up, and the advisory is specific about them. The plugin's Log Settings need "Enable Logs" turned on, and an administrator, or another user with access to the Log Settings screen, needs to open the polluted log file afterward. That view action is what actually calls wpfnl_show_log and executes the planted code.
That sounds like it narrows the field. It doesn't narrow it as much as it looks. The step that plants the payload, submitting the crafted postData value, requires no authentication at all: the nonce the funnel endpoint checks is printed into the HTML of every funnel step page, visible to anyone who loads it. An attacker can grab that nonce, submit the payload, and wait. On a store where logging is already switched on for troubleshooting checkout or upsell problems, which is common, the log gets viewed sooner or later. That's the moment the exploit completes.
This two-stage shape matters for how you think about exposure. A vulnerability that needs a specific setting on and a specific admin action afterward can read as low risk if you only count the second half. Here the first half, planting the payload, happens the moment an unauthenticated request hits the funnel endpoint, whether or not anyone ever opens the log. A site can carry a live PHP payload sitting in a log file for weeks before an admin's routine click on Log Settings triggers it. Scanning a site for the vulnerable version tells you whether the door was open; it doesn't tell you whether someone already walked through it.
Why logs are data, not code
The specific bug here is a WPFunnels bug. The pattern behind it isn't. Any WordPress plugin that logs request data and later renders that log on an admin screen makes the same two decisions: how to write the entry, and how to read it back. Get either one wrong on its own and nothing happens. Get both wrong and the log stops being a record of what occurred and becomes a script waiting for someone to run it.
The fix is one line of discipline: a log file is data. Read it with a function built for reading data, and treat whatever comes out of it as untrusted before printing it.
// Vulnerable: treats a log file as PHP source
include_once( $log_file_path );
// Safe: treats a log file as text, escapes it before output
$contents = file_get_contents( $log_file_path );
echo '<pre>' . esc_html( $contents ) . '</pre>';
file_get_contents() reads bytes. It never asks PHP to parse anything as code, so it doesn't matter what an attacker managed to write into the file. esc_html() then stops the log from running JavaScript in the admin's browser too, closing the smaller stored-XSS variant of the same mistake.
The extension buys no protection either. A web server that maps requests by file extension might refuse to serve a .log file directly over HTTP, but that's irrelevant here, since nobody is requesting the log file directly. PHP doesn't route through the extension when a script includes a path from inside the application; it runs whatever bytes sit at that path.
The same logic applies to any file a plugin writes into wp-content and later includes: a .txt cache a debug tool reopens, a .json config it "just happens" to eval(), a .csv export parsed with something more powerful than a CSV reader. None of those files need to look executable. They only need to be treated as executable somewhere downstream, in code the developer wrote for an entirely different purpose. Wordfence classifies this specific bug as CWE-434, Unrestricted Upload of File with Dangerous Type, which is the same weakness class behind file-upload RCEs, even though nothing here goes through an upload form. The log write is the upload; it just doesn't look like one.
wp-content is also where most plugins keep their working files by default, logs included, and that location is reachable by the same web server process handling every other request the site gets. A path traversal or an exposed direct-access script isn't required for this particular chain, since the plugin's own admin screen is the thing that reads the file. But the general point holds beyond this one plugin: a writable path under wp-content is a path the request that created it can often reach again later, through whatever code eventually opens it.
This is the same shape of bug behind the Breeze cache plugin RCE: a value the site never expected to double as PHP does exactly that once the wrong function touches it. Once that write-then-include chain lands, the result is usually a PHP backdoor sitting inside a file nobody thought to check, because nobody expects a log to run.
What to do now
Update WPFunnels to 3.12.8 or later. That release fixes both halves of the chain: it sanitizes what gets written to the log, and it switches the log viewer away from include_once(). If you're running WooCommerce with any WPFunnels version up to 3.12.7, treat this as an urgent update rather than a routine one, given the CVSS 9.8 rating and the fact that the injection step needs no login at all.
If your site ran a vulnerable version with logging enabled, check the plugin's log files before you delete or ignore them. Look for content that doesn't resemble a normal log entry: PHP opening tags (<?php), base64-encoded blobs, calls to eval(), or shell functions like system() or exec(). Any of that inside a .log file the plugin generated is a sign the write side of this bug was already triggered on your site, whether or not the log was ever viewed afterward.
Also check for files you don't recognize inside wp-content, particularly anything created or modified around the time you had logging turned on with an old version. An exploit chain like this one rarely stops at one payload run; code executed through wpfnl_show_log commonly drops a persistent backdoor so the attacker doesn't have to repeat the postData trick on every visit. Signs your WordPress site has malware covers what those artifacts tend to look like. For the broader mechanics of how one plugin flaw turns into a foothold, see how WordPress sites get hacked.
It's also worth checking whether "Enable Logs" is a setting you actually need on. If nobody on your team is actively troubleshooting a funnel issue, turning logging off removes the second half of this exploit chain entirely, since there's nothing for an admin to open. That's specific to how WPFunnels' log viewer works today; it isn't a substitute for updating, because the write side of the bug is fixed only in 3.12.8 and later. A site on an old version with logging off is still carrying the code path, just without the trigger currently switched on.
WP Vanguard flags outdated plugin versions and known CVEs like this one during a scan, which is one way to catch a vulnerable WPFunnels install before it turns into an incident report.
References
Related reading
Check Your WordPress Site Security
Free scan, no login required. Find vulnerabilities before attackers do.
Scan Your Site FreeGet weekly WordPress security tips
Vulnerability alerts, plugin updates, and security guides. No spam. Unsubscribe any time.