CVE-2026-94674 Breaking out of the data layer in Pixel Manager for WooCommerce

Intrudify's autonomous pentesting engine discovered a stored cross-site scripting vulnerability in Pixel Manager for WooCommerce, affecting all versions up to and including 1.69.0. The plugin writes a post title into an inline <script> element after HTML-entity-decoding it, using a JSON encoder that does not escape angle brackets. A Contributor can store a closing script tag as entities, which WordPress preserves and the plugin decodes back into real markup.

Discovered by
Intrudify autonomous engine
Validated & reported by
Tudor Lasuschevici
Disclosure
24 September 2026
Severity
Medium · CVSS 6.5
Vulnerability class
Stored XSS, CWE-79
Affected
Pixel Manager for WooCommerce ≤ 1.69.0
Fixed in
1.69.1
Active installs
50,000+
Privilege required
Contributor
Injection context
Inline <script> data layer
Configuration
Default, no pixel or setting required

Summary

Pixel Manager for WooCommerce prints a tracking data layer inside an inline script element on every front-end page. Intrudify's autonomous testing engine found that one of the values in that data layer is a post title, and that the title is HTML-entity-decoded immediately before it is placed there.

Because the JSON encoder is configured without JSON_HEX_TAG, a literal </script> inside that string is written out verbatim. It closes the script element, returns the HTML parser to data state, and everything after it is parsed as markup.

Technical detail

The sink

In Pixel_Manager::inject_data_layer():

$json_encode_options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
...
<script ...>
window.pmwDataLayer = Object.assign(window.pmwDataLayer,
<?php echo wp_json_encode( $this->get_data_for_data_layer(), $json_encode_options ); ?>);
</script>

JSON_HEX_TAG is not set, so PHP emits < and > as literal bytes rather than escaping them to \u003C and \u003E. Any string in the data layer containing a closing script tag therefore terminates the element.

The decode

One value in the data layer is entity-decoded right before it lands there. In Shop::pmw_get_the_title():

public static function pmw_get_the_title( $post = 0 ) {
    $post  = get_post( $post );
    $title = ( isset( $post->post_title ) ? $post->post_title : '' );
    return wp_specialchars_decode( $title );
}

wp_specialchars_decode() converts &lt; to < and &gt; to > even with its default argument, because those two entities sit in the unconditional table. For a blog post the result is assigned into the data layer as the list name.

Why the entities survive WordPress sanitising

This is the part that makes the payload storable. WordPress KSES runs on title_save_pre, and wp_kses_normalize_entities() disarms every ampersand and then re-instates every entity whose name is on its allowed list. lt and gt are both on that list.

So a Contributor stores a title that is, as far as the sanitiser is concerned, harmless text containing two named entities:

// stored post_title, byte for byte, by a Contributor
&lt;/script&gt;&lt;img src=x onerror=...&gt;

The plugin then decodes it back into real markup and writes it into a script context. The sanitiser and the sink disagree about whether those entities are data or markup, and the decode happens after sanitisation.

Nothing gates the injection

inject_data_layer() is hooked to wp_head from the class constructor with no guard, and the shop data is populated whenever WooCommerce is active, which the plugin requires anyway. No pixel, no setting and no license is involved, so a default installation is vulnerable out of the box.

Why only this sink

Worth noting for anyone auditing similar code: the plugin's other data-layer writer, in includes/class-product.php, encodes with JSON_HEX_TAG set. The developers knew about this class of problem and defended against it in one place. This sink was written with a different flag set and missed.

Impact

A Contributor cannot publish and holds no unfiltered_html capability, but the payload executes for anyone who renders the post. That includes the editor or administrator previewing the pending submission, which is the normal review step, and every anonymous visitor once the post is published.

We confirmed execution in the site origin on a default installation of WordPress 7.1, WooCommerce 11.1.1 and Pixel Manager 1.69.0, with no plugin settings changed. When the viewer is privileged, the usual consequences follow: session-scoped administrative actions, account creation, or plugin installation.

The same decode-then-embed pattern reaches this sink from three further sources: the page title, the product title, and the product category or tag name. Those require Editor or Shop Manager rather than Contributor, and they share the same root cause and the same fix. The post-title path is simply the lowest privilege of the four.

Remediation

Update Pixel Manager for WooCommerce to 1.69.1 or later.

For maintainers, there are two independent defects and fixing either breaks the chain. Add JSON_HEX_TAG to the encode options for the data layer, matching what the product-side writer already does. And stop calling wp_specialchars_decode() on a value destined for a script context, since re-introducing raw angle brackets undoes the only protection the stored form had.

The wider lesson

HTML entities in stored data are usually the safe form. Code that decodes them is normally trying to be helpful, restoring a human-readable title for display. The problem is that the decode moves the value from a safe representation to an unsafe one, and it happens after every sanitiser has already inspected and approved it.

The second half is context. A value that is harmless in an HTML text node can be fatal inside a script element, because the HTML parser terminates a script at the first </script> regardless of JavaScript string quoting. JSON encoding alone is not an escape for that context, which is exactly why JSON_HEX_TAG exists.

The generalisable check: search a codebase for entity-decoding functions and ask where each decoded value ends up. Any path from wp_specialchars_decode() or html_entity_decode() into a script or attribute context deserves scrutiny, and any inline JSON data layer should be encoded with tag escaping enabled without exception.

Disclosure timeline

  • 2026-09-19 Vulnerability identified and validated by Intrudify, reported via Patchstack
  • 2026-09-24 Published by Patchstack
  • 2026-09-30 CVE-2026-94674 assigned and record published

Questions

What is CVE-2026-94674?

CVE-2026-94674 is a stored cross-site scripting vulnerability in the Pixel Manager for WooCommerce plugin, versions 1.69.0 and earlier. It was discovered autonomously by the Intrudify AI penetration testing engine. A Contributor can place a closing script tag in a post title as HTML entities, which WordPress preserves and the plugin decodes back into real markup before writing it into an inline script element with a JSON encoder that does not escape angle brackets.

Which versions of Pixel Manager for WooCommerce are affected?

All versions up to and including 1.69.0 are affected. No plugin setting, configured pixel or license is involved, so a default installation of WordPress, WooCommerce and the plugin is vulnerable. The issue is fixed in 1.69.1.

How do I fix CVE-2026-94674?

Update Pixel Manager for WooCommerce to version 1.69.1 or later. The underlying fix is to set the JSON_HEX_TAG flag when encoding the data layer, and to stop HTML-entity-decoding values that are placed into a script context.

References

More advisories from Intrudify

Join the Future of
AI-Driven Pentesting