CVE-2026-97298 Three call sites escaped, two not, in King Addons for Elementor
Intrudify's autonomous pentesting engine discovered a stored cross-site scripting vulnerability in King Addons for Elementor, affecting all versions up to and including 51.1.86. A helper builds a CSS class string from three widget settings. The plugin escapes that helper's output at three of the five places it is used, and prints it raw at the other two. A Contributor can terminate the class attribute and add an event handler.
Summary
King Addons for Elementor is a widget pack for the Elementor page builder. Intrudify's autonomous testing engine found a helper, duplicated across two widget classes, that concatenates three raw widget settings into a CSS class string.
Both widgets ship enabled in the free version and no plugin setting needs to be changed. The finding is not that the helper exists, but that the plugin's own escaping of it is applied inconsistently.
Technical detail
The helper
public function get_image_effect_class($settings)
{
$class = '';
if ('none' !== $settings['image_effects']) {
$class .= ' king-addons-' . $settings['image_effects'];
}
...
return $class;
}The same helper is duplicated in both widget files. None of the three settings it reads is validated on the way in.
Escaped in three places, raw in two
| Call site | Escaping |
|---|---|
| Media_Grid.php:7156 | escaped |
| WooCommerce_Grid.php:9911 | escaped |
| Posts_Grid.php:9866 | escaped |
| Image_Accordion.php:2359 | raw |
| Media_Grid.php:8033 | raw |
The first of the two vulnerable sites is the more instructive, because it looks defended:
class="king-addons-image-accordion-item elementor-repeater-item-<?php
echo esc_attr($item['_id']) . $this->get_image_effect_class($settings); ?>"The esc_attr() call closes before the concatenation, so it protects the repeater id and nothing else. The helper output is appended outside it and emitted verbatim. The second site has no escaping at all.
Three correct call sites against two incorrect ones places this as an omission rather than a design decision, and it is why the finding is credible as a bug rather than an argument about intent.
Why a dropdown value is not a dropdown
image_effects is declared as a select control with a fixed list of options, which is exactly why a reviewer would not expect it to carry a payload. But Elementor does not check server-side that a stored select value is one of the declared options, so the constraint exists only in the editor UI. A request that writes the document directly is not bound by it.
The control also carries no condition, so the stored value is always returned at render, and one of the two vulnerable widgets reads settings by a path where conditional filtering would not apply in any case.
The payload and the output
The payload contains no angle brackets, only quotes and attribute text, so the wp_kses_post() pass Elementor applies to users without unfiltered_html has no markup to strip:
class="king-addons-image-accordion-item elementor-repeater-item-itm1111
king-addons-x" autofocus tabindex="1" onfocus="..."
y=" king-addons-effect-size-medium"The quote ends the class attribute and the rest becomes real attributes. autofocus with tabindex fires the handler as soon as the page loads.
Verification
We reproduced the chain over HTTP alone on a clean installation of WordPress 7.1.2, Elementor 4.3.2 and King Addons 51.1.86, with the default theme, no other active plugin and default settings throughout. The attacking account was a stock Contributor holding edit_posts but neither publish_posts nor unfiltered_html.
The Contributor created a draft, read the Elementor nonce from their own editor screen, saved both widgets carrying the payload, and submitted the post for review. The stored document holds the value verbatim. When the administrator opened the submission, the response contained four injected handlers and the script executed in that session with no interaction. After publication, an anonymous request with no cookie returned the same broken-out attribute.
The negative control is the strongest part
Four values were sent through the identical path, each to a fresh draft, and the rendered class attribute read back:
| Value sent | Result |
|---|---|
| zoom-in | renders as a normal class, nothing fires |
| <script> payload | reduced to inert text by kses |
| <img onerror> payload | handler stripped by kses |
| quote-only break-out | executes |
This shows Elementor's sanitisation working correctly on the two payloads it is designed to catch, and demonstrates precisely why the third gets through: there is no markup for wp_kses_post() to remove, so the quotes arrive intact at an attribute that is never escaped. Ruling out the obvious explanations is what makes the remaining one convincing.
Impact
A Contributor may only submit drafts for review and holds neither publish_posts nor unfiltered_html. Through these two widgets that role executes arbitrary JavaScript in the browser of the administrator or editor who opens the submission, inside that user's authenticated session.
From there the attacker creates a new administrator through the user-creation screen or installs a plugin, using the victim's own session and nonces. Once the post is published the same script runs for every site visitor.
Remediation
Update King Addons for Elementor to 51.1.87 or later.
For maintainers, escape the helper output at the two call sites that do not, matching the three that already do. Better, move the escaping inside the helper so no call site can get it wrong. And validate the three settings server-side against their declared options, since a select control's option list is a UI affordance rather than an enforced constraint.
The wider lesson
Escaping at the call site rather than at the source means every call site is a separate opportunity to forget, and the count here is three to two. A helper that returns a string destined for markup should either return it already escaped or be named so that its obligations are obvious. Neither is true of a function called get_image_effect_class(), whose name suggests it returns a class, which is something a developer reasonably assumes is safe.
The more broadly useful point is about control types. A select control feels like a closed set: the editor offers a dropdown, so the value must be one of the options. That intuition is correct about the editor and wrong about the data. Elementor does not enforce the option list server-side, so any field declared as a select, a switcher or a choose control is in practice a free-text field to anyone who writes the document directly. A reviewer auditing for injection will skip those fields precisely because they look constrained.
That is the fourth finding in this research set where escaping was present but misdirected, and the pattern across all four is the same: the code contains a security call, the reviewer sees it, and nobody checks whether it covers the value that actually reaches the sink.
Disclosure timeline
- 2026-09-24 Vulnerability identified and full chain validated by Intrudify, reported via Patchstack
- 2026-09-30 Public disclosure · CVE-2026-97298 assigned
Questions
What is CVE-2026-97298?
CVE-2026-97298 is a stored cross-site scripting vulnerability in the King Addons for Elementor WordPress plugin, versions 51.1.86 and earlier. It was discovered autonomously by the Intrudify AI penetration testing engine. A helper builds a CSS class string from widget settings, and two of the five places that use it print the result inside a class attribute with no escaping, letting a Contributor terminate the attribute and add an event handler.
Which widgets are affected by CVE-2026-97298?
The Image Accordion and Advanced Image Gallery widgets, both of which ship enabled in the free version. No plugin setting needs to be changed.
How do I fix CVE-2026-97298?
Update King Addons for Elementor to version 51.1.87 or later. The underlying fix is to escape the helper output at the two unescaped call sites, matching the three that already do, and to validate the select values server-side against their declared options rather than trusting the editor's dropdown.