{"repro_id":"REPRO-2026-00203","version":8,"title":"JetWidgets For Elementor Stored XSS via Animated Box animation_effect","repro_type":"security","status":"published","severity":"medium","description":"The JetWidgets For Elementor plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to and including 1.0.21. This is due to insufficient output escaping and missing server-side validation of the Animated Box widget's animation_effect setting before it is rendered inside an HTML class attribute. This makes it possible for authenticated attackers, with author-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.","root_cause":"# RCA Report: CVE-2026-11380 — JetWidgets For Elementor Stored XSS\n\n## Summary\n\nThe JetWidgets For Elementor WordPress plugin (versions up to and including 1.0.21) is vulnerable to a stored cross-site scripting (Stored XSS) flaw in the Animated Box widget. The widget's `animation_effect` setting is printed directly into the HTML `class` attribute of the rendered widget without output escaping or server-side validation. An attacker who can supply a value containing a double quote can break out of the `class` attribute and inject arbitrary attributes, including event handlers such as `onmouseover=\"alert(1)\"`. Because the payload is stored in the page and rendered whenever anyone visits the page, this is a stored XSS issue. The reproduction confirmed the payload in the published page HTML by deploying a real WordPress + Elementor + JetWidgets stack and viewing the rendered page.\n\n## Impact\n\n- **Product / component:** `jetwidgets-for-elementor` (Crocoblock / jetmonsters) — specifically the `jw-animated-box` widget.\n- **Affected versions:** 1.0.21 and earlier (the WordPress.org repository still lists 1.0.21 as the current stable release at the time of the CVE).\n- **Risk level:** Medium. The vendor/Wordfence CVSS is reported as 6.4.\n- **Consequences:** Any authenticated user with at least the `author` role can save a page containing a malicious Animated Box widget. The injected JavaScript is stored in the page and executes in the browser of any visitor who views the page.\n\n## Impact Parity\n\n- **Disclosed / claimed maximum impact:** Stored XSS via an author-level account in the Animated Box widget's `animation_effect` setting.\n- **Reproduced impact from this run:** A real WordPress environment was deployed with Elementor and JetWidgets 1.0.21. A published page was created with a malicious `animation_effect` value. The rendered page HTML contains the escaped attribute boundary break and the injected `style` and `onmouseover=\"alert(1)\"` attributes.\n- **Parity:** `full` — the reproduction reaches the same rendered-page sink described in the advisory.\n- **Not demonstrated:** The reproduction does not run a full browser engine, so the JavaScript payload itself is not executed in the test environment. The proof relies on the presence of the injected event handler in the server-rendered HTML, which is the root cause of the stored XSS.\n\n## Root Cause\n\nThe vulnerability is in the widget's render template:\n\n```php\n<!-- templates/jw-animated-box/global/index.php (v1.0.21) -->\n<div class=\"jw-animated-box <?php $this->__html( 'animation_effect', '%s' ); ?>\">\n```\n\nThe helper method chain ends up in `includes/base/class-jet-widgets-base.php`:\n\n```php\npublic function __render_html( $setting = null, $format = '%s' ) {\n    ...\n    printf( wp_kses_post( $format ), $val );\n}\n```\n\n`wp_kses_post()` is applied to the format string `'%s'`, not to the value. Therefore the raw `animation_effect` value is printed verbatim inside the HTML `class` attribute. The UI control in `includes/addons/jet-widgets-animated-box.php` is a `<select>` with a fixed set of animation effects, but the server never validates the stored value before rendering it. A malicious author can supply a value such as:\n\n```\njw-box-effect-1\" style=\"position:fixed;width:100%;height:100%;top:0;left:0\" onmouseover=\"alert(1)\" data-x=\"\n```\n\nThe first double quote closes the `class` attribute, the injected `style` covers the viewport, and the `onmouseover` handler will execute when a visitor moves the mouse over the page.\n\nThe vendor fixed the issue in commit `49952dd92b2bbd59e6627e7b67b0b3621c3852a0` (released as 1.0.22). The fix:\n\n1. Introduces a server-side `sanitize_animation_effect()` method that rejects any value not in the allowed list of animation effects and falls back to the default.\n2. Updates the template to use `esc_attr()` on the sanitized value:\n\n```php\n$animation_effect = $this->sanitize_animation_effect( $this->get_settings_for_display( 'animation_effect' ) );\n?>\n<div class=\"jw-animated-box <?php echo esc_attr( $animation_effect ); ?>\">\n```\n\n## Reproduction Steps\n\nThe full reproduction is automated by `bundle/repro/reproduction_steps.sh`. At a high level the script:\n\n1. Creates a Docker network and starts a `mysql:8.0` container and a `wordpress:6.7-php8.1-apache` container.\n2. Waits until the WordPress install page is reachable inside the Docker network.\n3. Installs WP-CLI, completes WordPress core installation, and sets the site URL to the internal Docker hostname (`http://jetwidgets-repro-wp`).\n4. Installs and activates Elementor 3.27.5 and JetWidgets For Elementor 1.0.21.\n5. Creates an `author` role user.\n6. Runs a PHP script inside the WordPress container that inserts a published page containing an Elementor `jw-animated-box` widget whose `animation_effect` setting is the malicious payload.\n7. Fetches the rendered page from inside the Docker network using `curl` and copies the body and headers to `bundle/repro/artifacts/`.\n8. Checks that the rendered HTML contains `onmouseover=\"alert(1)\"` and the injected `style` attribute.\n\n**Expected evidence of reproduction:** `bundle/repro/artifacts/page.html` contains a line similar to:\n\n```html\n<div class=\"elementor-jw-animated-box jet-widgets\"><div class=\"jw-animated-box jw-box-effect-1\" style=\"position:fixed;width:100%;height:100%;top:0;left:0\" onmouseover=\"alert(1)\" data-x=\"\">\n```\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — step-by-step console output from the reproduction run.\n- `bundle/logs/insert.log` — page ID and permalink produced by the PHP insertion script.\n- `bundle/logs/wordpress.log` — Apache/WordPress container logs.\n- `bundle/logs/mysql.log` — MySQL container logs.\n- `bundle/repro/artifacts/page.html` — full HTML body of the rendered published page.\n- `bundle/repro/artifacts/page.headers` — HTTP response headers from the page request.\n- `bundle/repro/artifacts/insert_page.php` — the PHP helper used to create the malicious page.\n\nKey excerpt from `bundle/repro/artifacts/page.html` (line 229 in the reproduced output):\n\n```html\n<div class=\"elementor-jw-animated-box jet-widgets\"><div class=\"jw-animated-box jw-box-effect-1\" style=\"position:fixed;width:100%;height:100%;top:0;left:0\" onmouseover=\"alert(1)\" data-x=\"\">\n```\n\nThis shows that the `animation_effect` value broke out of the `class` attribute and added new attributes, including the `onmouseover` event handler.\n\nEnvironment details captured in the runtime manifest:\n\n- WordPress 6.7 (PHP 8.1 / Apache)\n- Elementor 3.27.5\n- JetWidgets For Elementor 1.0.21\n- MySQL 8.0\n\n## Recommendations / Next Steps\n\n1. **Upgrade:** Site owners should upgrade JetWidgets For Elementor to version 1.0.22 or later, which contains commit `49952dd92b2bbd59e6627e7b67b0b3621c3852a0`.\n2. **Fix approach:**\n   - Server-side validation: reject any `animation_effect` value that is not in the defined list of allowed effects (e.g., `jw-box-effect-1` through `jw-box-effect-8`).\n   - Output escaping: render the value with `esc_attr()` before placing it inside the HTML `class` attribute.\n3. **Testing recommendations:**\n   - Add unit/integration tests that save payloads containing `\"`, `<`, `>`, and `javascript:` in the `animation_effect` setting and assert that the rendered HTML does not contain an unescaped attribute break or event handler.\n   - Test with the Elementor editor save flow as well as direct post-meta injection to ensure both paths are sanitized.\n4. **Further verification:** The script could be extended to include a real browser engine (e.g., Playwright/Chromium) to demonstrate actual JavaScript execution on mouseover. The current proof is sufficient to confirm the server-side sink but not a live browser exploit.\n\n## Additional Notes\n\n- **Idempotency:** The reproduction script was executed twice consecutively from a clean state and succeeded both times. It cleans up all Docker containers and networks on exit via a `trap`.\n- **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.\n- **No custom exploit code:** The reproduction uses the real WordPress, Elementor, and JetWidgets plugin code; no reimplementation of the vulnerability was used.\n","cve_id":"CVE-2026-11380","source_url":"jetmonsters/jetwidgets-for-elementor","reproduced_at":"2026-07-02T19:28:31.975916+00:00","duration_secs":1767.0,"tool_calls":176,"handoffs":2,"total_cost_usd":2.0325227599999995,"agent_costs":{"hypothesis_generator":0.04569565,"judge":0.424264,"repro":0.6284125700000001,"support":0.06051446,"vuln_variant":0.8736360799999998},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.04569565},"judge":{"gpt-5.5":0.424264},"repro":{"accounts/fireworks/models/kimi-k2p7-code":0.6284125700000001},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.06051446},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":0.8736360799999998}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-02T19:28:32.793652+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8199,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8471,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8654,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":11273,"category":"analysis"},{"path":"bundle/ticket.md","filename":"ticket.md","size":725,"category":"ticket"},{"path":"bundle/ticket.json","filename":"ticket.json","size":1110,"category":"other"},{"path":"bundle/repro/artifacts/insert_page.php","filename":"insert_page.php","size":1729,"category":"other"},{"path":"bundle/repro/artifacts/page.html","filename":"page.html","size":58157,"category":"other"},{"path":"bundle/repro/artifacts/page.headers","filename":"page.headers","size":446,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":693,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":843,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3897,"category":"log"},{"path":"bundle/logs/wordpress.log","filename":"wordpress.log","size":2068,"category":"log"},{"path":"bundle/logs/mysql.log","filename":"mysql.log","size":6492,"category":"log"},{"path":"bundle/logs/insert.log","filename":"insert.log","size":52,"category":"log"},{"path":"bundle/logs/vuln_variant/variant_reproduction.log","filename":"variant_reproduction.log","size":3412,"category":"log"},{"path":"bundle/logs/vuln_variant/insert_vuln.log","filename":"insert_vuln.log","size":69,"category":"log"},{"path":"bundle/logs/vuln_variant/wordpress_vuln.log","filename":"wordpress_vuln.log","size":2191,"category":"log"},{"path":"bundle/logs/vuln_variant/mysql_vuln.log","filename":"mysql_vuln.log","size":6492,"category":"log"},{"path":"bundle/logs/vuln_variant/insert_fixed.log","filename":"insert_fixed.log","size":70,"category":"log"},{"path":"bundle/logs/vuln_variant/wordpress_fixed.log","filename":"wordpress_fixed.log","size":2194,"category":"log"},{"path":"bundle/logs/vuln_variant/mysql_fixed.log","filename":"mysql_fixed.log","size":6492,"category":"log"},{"path":"bundle/vuln_variant/artifacts/insert_vuln.php","filename":"insert_vuln.php","size":2228,"category":"other"},{"path":"bundle/vuln_variant/artifacts/insert_fixed.php","filename":"insert_fixed.php","size":2228,"category":"other"},{"path":"bundle/vuln_variant/artifacts/page_vuln.html","filename":"page_vuln.html","size":58849,"category":"other"},{"path":"bundle/vuln_variant/artifacts/page_vuln.headers","filename":"page_vuln.headers","size":443,"category":"other"},{"path":"bundle/vuln_variant/artifacts/page_fixed.html","filename":"page_fixed.html","size":58892,"category":"other"},{"path":"bundle/vuln_variant/artifacts/page_fixed.headers","filename":"page_fixed.headers","size":446,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":7380,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1101,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3140,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":886,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1011,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1316,"category":"other"}]}