# Patch Analysis: CVE-2026-11380 Fix vs. JetWidgets For Elementor Pricing Table Variant

## Affected Product / Version

- **Product:** JetWidgets For Elementor (WordPress plugin)
- **Vulnerable version tested:** 1.0.21
- **Patched/fixed version tested:** 1.0.22 (SVN tag `tags/1.0.22`, revision 3594346, WordPress.org)
- **Latest version at time of analysis:** 1.0.22

## What the Vendor Fix Changes

The vendor's 1.0.22 release is intended to close the stored XSS reported as CVE-2026-11380. The changes are localized to the **Animated Box** widget and a small part of the **Pricing Table** button rendering:

1. **`includes/addons/jet-widgets-animated-box.php`**
   - Adds `get_animation_effect_options()`, `get_default_animation_effect()`, and `sanitize_animation_effect()`.
   - `sanitize_animation_effect()` checks whether the stored value is a string and exists in the allow-list of eight animation-effect keys (`jw-box-effect-1` … `jw-box-effect-8`). If not, it falls back to the default.
   - Adds `sanitize_button_icon_position()` for the button icon position control (`before` / `after`).

2. **`templates/jw-animated-box/global/index.php`**
   - Replaces the raw `<?php $this->__html( 'animation_effect', '%s' ); ?>` class-fragment output with:
     ```php
     $animation_effect = $this->sanitize_animation_effect( $this->get_settings_for_display( 'animation_effect' ) );
     ?>
     <div class="jw-animated-box <?php echo esc_attr( $animation_effect ); ?>">
     ```
   - The stored value is now validated against the allow-list and escaped with `esc_attr()` before being placed in the HTML `class` attribute.

3. **`templates/jw-pricing-table/global/button.php` and `includes/addons/jet-widgets-pricing-table.php`**
   - Adds `sanitize_button_size()` and `sanitize_button_icon_position()` for the pricing-table button.
   - Replaces raw `get_settings( 'button_size' )` concatenation in a class name with the sanitized value, and uses Elementor's `print_render_attribute_string()` instead of the raw `jet_widgets_tools()->esc_attr()` wrapper.

## What the Fix Assumes

The fix assumes that the security problem is **limited to enumerated SELECT-style controls** whose values are later rendered into HTML class attributes. It assumes that if every control value is either:
- restricted to a known allow-list, and/or
- escaped with `esc_attr()` at the final output location,

then the stored XSS surface is eliminated.

It also assumes that only the Animated Box `animation_effect` and the Pricing Table button `button_size` / `button_icon_position` need this treatment, because those were the specific SELECT controls involved in the reported CVE.

## What the Fix Does NOT Cover

The fix does **not** address the underlying pattern in `Jet_Widgets_Base` that makes raw stored values printable: `__html()`, `__get_html()`, and `__loop_item()` all call `printf()` (or `sprintf()`) with the format string passed through `wp_kses_post()`, but the actual user-provided value is inserted raw. `__loop_item()` only applies a sanitizer if the caller explicitly passes one; otherwise the value is returned verbatim.

Consequently, many other widget templates that use these helpers remain vulnerable. The most directly comparable path is in the **Pricing Table** widget:

- **File:** `templates/jw-pricing-table/global/features-loop-item.php`
- **Sink:**
  ```php
  printf( '<span class="pricing-feature__text">%s</span>', $this->__loop_item( array( 'item_text' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  ```
- **Control:** `item_text` is registered as a `Controls_Manager::TEXT` control in the `features_list` repeater (`includes/addons/jet-widgets-pricing-table.php`). It is intended to hold plain text, not HTML.
- **Why it is a bypass:** The value is not passed through any sanitizer (`__loop_item` is called with only the key and the format), and the surrounding `printf()` does not escape it. Even though the value is rendered as text content rather than inside an attribute, unescaped HTML tags are emitted verbatim, producing stored XSS. This file is **byte-for-byte identical** in 1.0.21 and 1.0.22.

A broader audit would also flag other `__html()` calls in the same plugin (e.g., pricing table `title`, `subtitle`, `price_desc`, services `description`, etc.), but the `features_list/item_text` path is the one demonstrated end-to-end as a bypass of the 1.0.22 patch.

## Threat Model / Scope

The plugin does not publish a `SECURITY.md` or formal threat model. Its `readme.txt` and `README.md` changelog show a multi-year pattern of adding "sanitize & escape output" fixes (e.g., 1.0.21, 1.0.19, 1.0.18, 1.0.17, 1.0.16, 1.0.14, 1.0.13, 1.0.9). This indicates the vendor treats unescaped widget output as in-scope for security maintenance. The WordPress plugin security model also treats author-level stored XSS as a vulnerability, because the author role is not allowed to execute unfiltered HTML/JavaScript by default.

## Comparison of Behavior Before and After the Fix

- **Animated Box `animation_effect`:** On 1.0.21, a malicious `animation_effect` value such as `jw-box-effect-1" style="..." onmouseover="alert(1)"` breaks out of the `class` attribute. On 1.0.22, the value is rejected by the allow-list and falls back to the default, then escaped with `esc_attr()`. The original payload no longer executes.
- **Pricing Table `features_list` `item_text`:** On 1.0.21, a value such as `<script>alert(1)</script>` is rendered verbatim inside `<span class="pricing-feature__text">`. On 1.0.22, the exact same behavior occurs because the `features-loop-item.php` template and the `__loop_item()` helper were not changed. The 1.0.22 patch is therefore incomplete for this path.

## Is the Fix Complete?

No. The fix is **partial** and **localized**. It addresses the specific control named in the CVE but leaves the shared rendering helpers and many other widget templates unchanged. The variant described here proves that an attacker with author-level access can still inject stored JavaScript through the Pricing Table widget on the latest released version (1.0.22).

## Recommendations for Closing the Gap

1. **Escape in the template layer:** In `templates/jw-pricing-table/global/features-loop-item.php`, change the sink to:
   ```php
   printf( '<span class="pricing-feature__text">%s</span>', esc_html( $this->__loop_item( array( 'item_text' ) ) ) );
   ```
   (or use `wp_kses_post()` if the field is intended to support safe HTML). Since the control is a plain text field, `esc_html()` is appropriate.

2. **Fix the helpers:** Consider making `__html()`, `__get_html()`, and `__loop_item()` apply context-appropriate escaping by default, with an explicit opt-out for callers that have already escaped the value. This removes the burden from every template and prevents future regressions.

3. **Audit all templates:** Review every remaining `__html()` / `__loop_item()` call and every `printf()` / `sprintf()` that embeds a stored widget setting. Ensure the value is escaped according to its output context (attribute vs. text content vs. URL vs. HTML).

4. **Add regression tests:** For each text-like control, save a payload containing `<script>`, `"`, `>`, and `javascript:` through both the Elementor editor and direct post-meta injection, and assert that the rendered page does not contain unescaped active content.
