Automated Threat Modeling

Overview of CVE-2026-107750: Entity Encoding Bypass Leads to Stored XSS in PDF Invoices & Packing Slips for WooCommerce

CVE-2026-107750 is an unauthenticated stored XSS in PDF Invoices & Packing Slips for WooCommerce through 5.16.4. Entity-encoded payloads bypass the earlier CVE-2026-92244 fix. This post summarizes the flaw and reviews the 5.16.5 patch.

CVE Analysis

9 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-03

Overview of CVE-2026-107750: Entity Encoding Bypass Leads to Stored XSS in PDF Invoices & Packing Slips for WooCommerce
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A guest shopper's billing name should never be able to run script in the browser of whoever later views that order's documents, yet that is the exposure documented in CVE-2026-107750 for the PDF Invoices & Packing Slips for WooCommerce plugin. This one is a bypass of an earlier fix. The sanitization added for CVE-2026-92244 in version 5.16.2 worked as designed, and a decode step later in the rendering pipeline undid it.

PDF Invoices & Packing Slips for WooCommerce generates invoice and packing slip documents from WooCommerce order data. It is developed by WP Overnight, a WordPress and WooCommerce plugin shop acquired by UpdraftPlus in January 2023. WordPress.org lists the plugin at 300,000+ active installations, and WPScan reports a similar figure of 300,000. Because the plugin sits on the path between customer-supplied checkout data and generated document output, flaws in how it handles that data affect a substantial number of WooCommerce stores.

Technical Information

At a glance

AttributeValue
WeaknessCWE-79, improper neutralization of input during web page generation
Affected versionsAll versions up to and including 5.16.4
Attacker authenticationNone required
Entry pointsBilling first name, last name, company, and customer note at guest checkout
CVSS 3.17.2 High, supplied by Wordfence as CNA
VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
First patched version5.16.5

Two notes on scoring. First, 7.2 is the Wordfence CNA score. NIST has not supplied its own assessment. Second, the published vector says UI:N, while the advisory narrative states that the script executes when a user accesses an injected page. We preserve both statements here rather than substituting a different vector.

Root cause: sanitize first, decode later

Version 5.16.2 addressed CVE-2026-92244 by applying a wp_kses() allow list to customer-supplied billing fields. kses works by recognizing literal tags, so a raw <img onerror=...> payload gets stripped. The problem was what happened after the template was rendered. OrderDocument::get_html() ran a document-wide cleanup pass over the finished HTML:

// clean up special characters if ( apply_filters( 'wpo_wcpdf_convert_encoding', function_exists( 'htmlspecialchars_decode' ) ) ) { $html = htmlspecialchars_decode( wcpdf_convert_encoding( $html ), ENT_QUOTES ); }

Now consider an entity-encoded payload such as &lt;img src=x onerror=...&gt;. It contains no literal angle brackets, so kses has nothing to match and passes it through unmodified. The template prints it, and at that moment it is harmless text. Then the final htmlspecialchars_decode() converts &lt; and &gt; back into real < and >, and the payload becomes live markup in the generated HTML.

The vulnerable 5.16.3 source corroborates this. OrderDocument.php renders the document template and then executes htmlspecialchars_decode( wcpdf_convert_encoding( $html ), ENT_QUOTES ), and BulkDocument.php contains the same decoding operation when assembling bulk document HTML. The decode is gated on the wpo_wcpdf_convert_encoding filter, whose default is simply whether htmlspecialchars_decode exists, so the decode runs unless a site has explicitly disabled it.

The sanitizer was there. The failure was in processing order: the input was sanitized, and a transformation that ran after sanitization reversed the effect. OWASP's XSS Prevention Cheat Sheet warns that modifying content after it has been sanitized can invalidate the protection. The security boundary is the HTML ultimately delivered for interpretation, and an intermediate string passing a filter does not establish that the final HTML is safe.

Attack flow

Based on the advisory and NVD description, the documented sequence is:

  1. An unauthenticated attacker places an order through guest checkout and supplies entity-encoded markup in the billing first name, last name, company, or customer note field.
  2. The 5.16.2 allow list processes the value but never encounters the literal tag delimiters it is designed to block, so the value is stored as is.
  3. When a document for that order is rendered, the template inserts the value, and the trailing htmlspecialchars_decode() pass reconstructs the dangerous markup.
  4. The injected script executes when a user accesses the affected page.

Scope of impact and exploitation status

The advisory establishes arbitrary script execution in pages. It does not establish that every PDF reader executes the payload, enumerate every viewing route, or document server-side code execution, administrator takeover, or a data theft incident.

On exploitation, TheHackerWire reports that no public exploit code has been confirmed and that the CVE is not recorded in CISA's Known Exploited Vulnerabilities catalog. These are secondary reporting statements, and we could not obtain primary KEV confirmation. No named threat actor or campaign is identified. Wordfence credits the researcher churchill11 with the finding.

Patch Information

WP Overnight fixed this in PDF Invoices & Packing Slips for WooCommerce 5.16.5, released October 20, 2026. Wordfence lists 5.16.5 as the patched version, and the CVE record points to WordPress.org SVN changeset 3756047 as the fix. The changelog entry for 5.16.5 is short: "Fix: Harden document output and shortcode order access". That release bundles more than one fix, and this section covers only the document output change behind CVE-2026-107750. The latest release is 5.16.6, which fixes an unrelated email attachment bug.

Keep the decode, escape customer text so it survives

The patch keeps the decode pass, which the plugin still needs for normal special characters, and makes customer-controlled values survive it. A new helper in includes/Documents/OrderDocument.php doubles every ampersand in customer text, but only while a document is being rendered:

public function protect_from_document_decode( $value ) { if ( ! $this->protect_from_decode ) { return $value; } if ( is_string( $value ) ) { return str_replace( '&', '&amp;', $value ); } if ( is_array( $value ) ) { return array_map( array( $this, 'protect_from_document_decode' ), $value ); } return $value; }

With this in place, &lt;img...&gt; becomes &amp;lt;img...&amp;gt; before it reaches the template. The single htmlspecialchars_decode() peels off one layer and leaves &lt;img...&gt;, which renders as inert text. Plain values like "Smith & Sons" come out of the decode exactly as they went in.

The second change is in get_html(), which now controls when that protection is switched on:

$decode_html = (bool) apply_filters( 'wpo_wcpdf_convert_encoding', function_exists( 'htmlspecialchars_decode' ) ); $this->protect_from_decode = $decode_html; try { $html = $this->render_template( ... ); } finally { $this->protect_from_decode = false; } ... if ( $decode_html && $args['decode_html'] ) { $html = htmlspecialchars_decode( wcpdf_convert_encoding( $html ), ENT_QUOTES ); }

Three details stand out:

  • The protection is tied to the decode. It only turns on if the wpo_wcpdf_convert_encoding filter would actually run the decode. If a site has disabled decoding, values are not escaped an extra time for no reason.
  • The try/finally always resets the flag. It returns to false even if template rendering throws an exception, so the helper never leaks escaping into code outside document rendering.
  • A new decode_html argument (default true) lets callers skip the decode pass. The bulk document fix below depends on it.

Which outputs are covered

In includes/Documents/OrderDocumentMethods.php, the patch wraps every output that carries customer-controlled text:

// billing & shipping address getters - return $address; + return $this->protect_from_document_decode( $address ); // billing custom field + return $this->protect_from_document_decode( apply_filters( 'wpo_wcpdf_billing_custom_field', $custom_field, $this ) ); // document notes + <?php echo $this->protect_from_document_decode( wpo_wcpdf_sanitize_html_content( $content, 'notes' ) ); ?> // order item meta + $data['meta'] = $this->protect_from_document_decode( wpo_ips_display_item_meta( $item, ... ) ); // shipping notes (customer note) + return $this->protect_from_document_decode( apply_filters( 'wpo_wcpdf_shipping_notes', $shipping_notes, $this ) );

This covers the billing first name, last name, and company (through the formatted address), the customer note, and several neighbouring sinks that had the same flaw. Wrappers around wpo_wcpdf_billing_custom_field and wpo_wcpdf_shipping_notes are applied after third-party filters run, so anything a filter adds is protected as well.

Closing the bulk export gap

When invoices are exported in bulk, the plugin builds one HTML file from each order's get_html() output, and the bulk wrapper performs its own special character cleanup. If each per-order call also decoded, content would be decoded twice. One layer of &amp; cannot survive two decodes, and the payload would return. includes/Documents/BulkDocument.php now tells each per-order render to skip its own decode:

- $html_content[ $key ] = $document->get_html( array( 'wrap_html_content' => false ) ); + $html_content[ $key ] = $document->get_html( array( 'wrap_html_content' => false, 'decode_html' => false ) );

The per-order render still escapes customer values in advance, because the flag follows the filter, not the argument. Each piece of content is escaped once and decoded once.

The takeaway

The 5.16.2 fix sanitized input where it entered. The 5.16.5 fix considers the whole output path and accounts for the transformation that runs after sanitization. For any template pipeline that applies a decode or normalization pass after values are inserted, escaping has to match the number of decode passes that will actually run on the output.

Affected Systems and Versions

  • Vulnerable: PDF Invoices & Packing Slips for WooCommerce, all versions up to and including 5.16.4.
  • Still vulnerable despite the earlier fix: 5.16.2, 5.16.3, and 5.16.4. These include the CVE-2026-92244 allow list but are bypassed by entity-encoded input.
  • First fixed: 5.16.5 (changeset 3756047, October 20, 2026).
  • Latest: 5.16.6, which fixes the separate CVE-2026-109217 (unauthenticated sensitive information exposure via the yearly reset invoice attachment cache), affecting versions through 5.16.5.
  • Relevant configuration: the injection path is guest checkout, and the vulnerable decode runs when the wpo_wcpdf_convert_encoding filter is enabled, which is the default when htmlspecialchars_decode is available.
ReleaseSecurity significance
5.16.2Fixed CVE-2026-92244 with a wp_kses() allow list, which this CVE bypasses
5.16.5First patched version for CVE-2026-107750
5.16.6Fixes the unrelated CVE-2026-109217, which matters when choosing an upgrade target

The two later advisories describe different issues and should not be conflated. Version 5.16.5 is the answer for this XSS, and 5.16.6 or newer covers both.

Vendor Security History

The CVE record lists the vendor as unknown, but NVD's affected product data names wpovernight, and WordPress.org lists WP Overnight among the plugin contributors. The plugin changelog records several prior security fixes:

ReleaseChangelog dateSecurity change
3.7.6January 9, 2024Potential SQL injection in Number Tools
3.7.7January 29, 2024SQL injection in Advanced Numbers search
3.8.1April 23, 2024XSS involving WooCommerce checkout address input
4.0.0February 4, 2025Document access security issue
5.16.22026Fix for CVE-2026-92244, stored XSS via billing name and company fields (through 5.16.1)
5.16.5October 20, 2026Fix for CVE-2026-107750, this bypass

The 2024 checkout address XSS and the 2026 billing field issues both involve customer-supplied data reaching document output. The record shows consistent patch response, and it also shows that a released fix can leave a related processing path exposed. We would not read more into it than that. The history does not by itself support a numerical rating of the vendor's development practices.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization