# Variant RCA Report: CVE-2026-11380 — Pricing Table `features_list` Stored XSS Bypass on JetWidgets 1.0.22

## Summary

The JetWidgets For Elementor 1.0.22 release was intended to fix CVE-2026-11380, a stored XSS in the Animated Box widget's `animation_effect` setting. The patch validates the `animation_effect` value against an allow-list and escapes it with `esc_attr()` before rendering it in the HTML `class` attribute. However, the patch does not address the same underlying pattern in other widgets. Specifically, the Pricing Table widget's `features_list` repeater still renders the `item_text` value through `__loop_item()` without any sanitization, and the final `printf()` does not escape the value. As a result, an authenticated author can store a feature item containing `<script>alert(1)</script>` and that script will be rendered verbatim on the published page. This variant reproduces on the patched version 1.0.22, making it a true bypass of the 1.0.22 fix.

## Fix Coverage / Assumptions

The 1.0.22 fix relies on the assumption that the vulnerability is limited to enumerated SELECT controls whose values are rendered into HTML class attributes. The patch explicitly covers:

- Animated Box `animation_effect` (`includes/addons/jet-widgets-animated-box.php` + `templates/jw-animated-box/global/index.php`)
- Animated Box `button_icon_position` (allow-listed)
- Pricing Table `button_size` and `button_icon_position` (`templates/jw-pricing-table/global/button.php` + `includes/addons/jet-widgets-pricing-table.php`)

The fix does **not** cover:

- The shared `__html()` / `__get_html()` / `__loop_item()` helper methods, which still print raw stored values by default.
- Other Pricing Table templates such as `features-loop-item.php`, `heading.php`, `price.php`, and `action.php` that use these helpers for text-like content.
- Any other widget template that calls `__loop_item()` without a sanitize callback.

## Variant / Alternate Trigger

- **Entry point:** A published WordPress page created by an author-level user containing a JetWidgets **Pricing Table** (`jw-pricing-table`) widget.
- **Attacker-controlled input:** The `item_text` field of the `features_list` repeater in the Pricing Table widget settings.
- **Data path:** The malicious string is stored in Elementor post meta (`_elementor_data`) and rendered when any visitor views the page.
- **Sink:** `templates/jw-pricing-table/global/features-loop-item.php`:
  ```php
  printf( '<span class="pricing-feature__text">%s</span>', $this->__loop_item( array( 'item_text' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  ```
- **Helper involved:** `Jet_Widgets_Base::__loop_item()` → `Jet_Widgets_Base::__render_loop_item()` in `includes/base/class-jet-widgets-base.php`. The helper applies a sanitizer only if the caller provides one; otherwise it returns `sprintf( $format, $value )` with the raw value.
- **Why this is a bypass:** This file is unchanged between 1.0.21 and 1.0.22. The 1.0.22 patch only fixes the Animated Box and the Pricing Table button controls, leaving the feature-text path open.

## Impact

- **Product / component:** `jetwidgets-for-elementor` (Crocoblock / jetmonsters), specifically the `jw-pricing-table` widget and the `features_list` repeater.
- **Affected versions:** 1.0.21 (vulnerable) and 1.0.22 (still vulnerable to this variant). 1.0.22 is the latest release at the time of analysis.
- **Risk level:** Medium (same as the parent CVE: stored XSS via author-level access).
- **Consequences:** Any authenticated user with at least the `author` role can save a page containing a malicious Pricing Table feature. The injected JavaScript is stored in the page and executes in the browser of any visitor who views the page.

## Impact Parity

- **Disclosed / claimed maximum impact for the parent CVE:** Stored XSS via an author-level account in the Animated Box widget's `animation_effect` setting.
- **Reproduced impact from this variant run:** A real WordPress environment was deployed with Elementor 3.27.5 and JetWidgets 1.0.22. A published page was created with a malicious Pricing Table `item_text` value. The rendered page HTML contains the raw `<script>alert(1)</script>` inside the feature text span.
- **Parity:** `full` — the variant reaches the same stored XSS impact class (author → stored script → viewer execution) through a different widget and setting.
- **Not demonstrated:** We did not demonstrate a different impact class (e.g., code execution on the server). The reproduced impact is the same as the parent claim.

## Root Cause

The root cause is the same as the parent CVE: a widget setting that is intended to be plain text is printed into the page without output escaping or server-side validation. The original fix added allow-list validation and `esc_attr()` only for the specific Animated Box control and two Pricing Table button controls. Because the underlying `__loop_item()` helper does not escape by default and the `features-loop-item.php` template does not call `esc_html()` or `wp_kses_post()` on the `item_text` value, the same stored XSS can be reached from a different entry point.

The vendor's fix commit referenced for the parent CVE is `49952dd92b2bbd59e6627e7b67b0b3621c3852a0` (released as 1.0.22). The tested 1.0.22 source is WordPress.org SVN tag `tags/1.0.22`, revision 3594346.

## Reproduction Steps

The full reproduction is automated by `bundle/vuln_variant/reproduction_steps.sh`. At a high level the script:

1. Spins up two isolated Docker stacks (WordPress 6.7 + PHP 8.1 + MySQL 8.0 + Elementor 3.27.5).
2. Installs JetWidgets For Elementor 1.0.21 in the first stack and 1.0.22 in the second stack.
3. Creates an author-level user in each stack.
4. Runs a PHP script inside each WordPress container that inserts a published page containing a Pricing Table widget with a malicious `features_list` item:
   ```php
   'features_list' => array(
       array(
           'item_text'     => '<script>alert(1)</script>',
           'item_included' => 'item-included',
       ),
   ),
   ```
5. Fetches the rendered page from inside each Docker network and copies the body and headers to `bundle/vuln_variant/artifacts/`.
6. Checks whether the raw `<script>alert(1)</script>` string appears in each rendered page.
7. Reports `BYPASS CONFIRMED` and exits 0 if the payload appears in the 1.0.22 rendered page.

**Expected evidence of reproduction:** `bundle/vuln_variant/artifacts/page_fixed.html` contains a line similar to:

```html
<div class="pricing-feature__inner"><span class="jet-widgets-icon item-bullet">...</span><span class="pricing-feature__text"><script>alert(1)</script></span></div>
```

The same payload appears in `bundle/vuln_variant/artifacts/page_vuln.html` for the 1.0.21 stack.

## Evidence

- `bundle/logs/vuln_variant/variant_reproduction.log` — step-by-step console output from the variant run (both 1.0.21 and 1.0.22).
- `bundle/logs/vuln_variant/insert_vuln.log` and `bundle/logs/vuln_variant/insert_fixed.log` — page IDs and permalinks produced by the PHP insertion scripts.
- `bundle/logs/vuln_variant/wordpress_vuln.log` / `mysql_vuln.log` — container logs for the vulnerable stack.
- `bundle/logs/vuln_variant/wordpress_fixed.log` / `mysql_fixed.log` — container logs for the patched stack.
- `bundle/vuln_variant/artifacts/page_vuln.html` and `bundle/vuln_variant/artifacts/page_vuln.headers` — rendered page from the 1.0.21 stack.
- `bundle/vuln_variant/artifacts/page_fixed.html` and `bundle/vuln_variant/artifacts/page_fixed.headers` — rendered page from the 1.0.22 stack.
- `bundle/vuln_variant/artifacts/insert_vuln.php` and `bundle/vuln_variant/artifacts/insert_fixed.php` — the PHP helpers used to create the malicious pages.

Key excerpt from `bundle/vuln_variant/artifacts/page_fixed.html` (line 232 in the reproduced output):

```html
<div class="pricing-feature__inner"><span class="jet-widgets-icon item-bullet"><svg aria-hidden="true" class="e-font-icon-svg e-fas-check" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg></span><span class="pricing-feature__text"><script>alert(1)</script></span></div>
```

Environment details:

- WordPress 6.7 (PHP 8.1 / Apache)
- Elementor 3.27.5
- JetWidgets For Elementor 1.0.21 (vulnerable) and 1.0.22 (patched, but bypassed)
- MySQL 8.0

## Recommendations / Next Steps

1. **Extend the fix to the Pricing Table feature text:** In `templates/jw-pricing-table/global/features-loop-item.php`, escape the `item_text` value before printing. Since the control is a plain text field, the appropriate fix is:
   ```php
   printf( '<span class="pricing-feature__text">%s</span>', esc_html( $this->__loop_item( array( 'item_text' ) ) ) );
   ```

2. **Audit all remaining `__html()` / `__loop_item()` calls:** Walk every widget template and helper call to ensure stored values are escaped according to their output context. Pay special attention to text content that should use `esc_html()` and URLs that should use `esc_url()`.

3. **Harden the base helpers:** Consider making `__html()`, `__get_html()`, and `__loop_item()` apply context-aware escaping by default, with an explicit `false` opt-out for callers that have already escaped. This prevents the same pattern from recurring every time a new control is added.

4. **Regression tests:** Add tests that save payloads containing `<script>`, `"`, `>`, and `javascript:` into each widget's text-like controls and assert that the rendered page escapes them. Test both the Elementor editor save flow and direct post-meta injection.

## Additional Notes

- **Idempotency:** The reproduction script was executed twice consecutively from a clean state and produced the bypass both times.
- **Clean state:** The script creates fresh Docker containers and networks for each run, then removes them via a `trap` on exit. It does not mutate the repro stage's artifacts or the plugin source checked out in `bundle/artifacts/`.
- **No custom exploit code:** The reproduction uses the real WordPress, Elementor, and JetWidgets plugin code; no reimplementation of the vulnerability was used.
- **Limitations:** The reproduction does not exercise the Elementor editor UI or the `admin-ajax.php` save endpoint. It sets the widget data directly in post meta. This is acceptable for proving the rendered-page vulnerability sink, but it does not reproduce the full author-workflow UI. The root cause is the same regardless of how the malicious value reaches the database.
- **Trust boundary:** The input is supplied by an authenticated author and rendered to anonymous visitors. This crosses the same trust boundary as the parent CVE and is within the plugin's stated security scope (the changelog repeatedly documents "sanitize & escape output" fixes).
- **Not a documented limitation:** The `item_text` control is registered as a plain TEXT control, not a WYSIWYG/HTML control. Rendering raw HTML tags from it is therefore not expected behavior.
