CVE-2026-97301 Escaping the value and printing the key in Cool FormKit Lite
Intrudify's autonomous pentesting engine discovered a stored cross-site scripting vulnerability in Cool FormKit Lite, affecting all versions up to and including 2.7.8. The Cool Form widget walks a stored settings array and prints each key as an HTML attribute name. esc_attr() is applied to that key, but it does not encode the space or the equals sign, which are the only characters needed to turn one attribute into several.
Summary
Cool FormKit Lite adds a form builder and Elementor form extensions to WordPress. Intrudify's autonomous testing engine found that every text-type field of its Cool Form widget is rendered by a method that iterates an internal settings array and emits each entry as an attribute, using the array key as the attribute name.
That key is attacker-controlled, because it is not a registered Elementor control and Elementor persists unknown settings keys verbatim. A Contributor who can save an Elementor document can plant it.
Technical detail
The sink
In modules/forms/classes/form-base.php:
foreach ( $item['custom_mask_attributes'] as $attr => $value ) {
if ( $attr !== 'class' ) {
echo esc_attr( $attr ) . '="' . esc_attr( $value ) . '" ';
}
}Both the name and the value pass through esc_attr(), so at a glance the line looks defended twice over. The value is genuinely safe. The name is not, because esc_attr() encodes only five characters and neither of the two that matter here is among them:
| Character | Encoded by esc_attr | Needed by the payload |
|---|---|---|
| < > & " ' | yes | no |
| space | no | yes |
| = | no | yes |
An attribute name containing a space and an equals sign is emitted verbatim, and the browser reads it as several attributes:
<input type="text" ... placeholder="Your name"
autofocus onfocus=... x="1" >autofocus focuses the field as soon as the document is parsed, so the handler fires with no user interaction at all.
Why a Contributor controls the key
custom_mask_attributes is not a registered Elementor control. It is an internal key that an input-mask add-on fills in when a mask is configured, and when no mask applies the add-on returns the repeater item untouched. Whatever the stored widget settings hold for that key therefore reaches the renderer unchanged.
Elementor persists unknown settings keys verbatim, so any user who can save an Elementor document can plant it with a crafted save request. That includes a Contributor. The Cool Form builder is enabled by default and only the free Elementor plugin is required.
Why the payload survives sanitisation
The payload is an attribute name. It contains no HTML tag, so the wp_kses_post pass Elementor applies on save for users without unfiltered_html finds nothing to remove. We confirmed against the running plugin that the stored document holds the injected key byte for byte.
A second location with the same root cause
The editor's Backbone template concatenates the attribute name raw as well, escaping only the value, so the injected attributes also render inside the Elementor editor preview. Any fix needs to cover both.
Impact
Contributor is the lowest role that can open the Elementor editor, and it is routinely handed to guest authors. This gives that role arbitrary JavaScript execution in the site origin.
Publication is not required. The payload executes as soon as an administrator or editor opens the preview of the pending submission, which is the ordinary review step for Contributor content, so the script runs inside an authenticated administrator session. From there, same-origin requests to admin-ajax or the REST API are a path to full site compromise. Once the post is published, the same handler executes for every visitor.
We reproduced the whole chain over HTTP alone against a default installation of the latest version.
Remediation
Update Cool FormKit Lite to 2.7.9 or later.
For maintainers, stop treating stored setting keys as HTML attribute names. Build the attribute list from a fixed allow-list, or validate each key against a strict pattern and drop anything that does not match, including any key beginning with on. Apply the same change to the editor template, which has the same defect.
The wider lesson
This is the second finding in this research set where esc_attr() is present, correct, and beside the point. There the protection was aimed at the wrong quote; here it is aimed at the wrong half of the attribute. In both cases a reviewer scanning for unescaped output sees an escaping call and moves on.
The specific lesson is that esc_attr() is named for attribute values and only makes sense there. Its character set exists to stop a value escaping its quotes. An attribute name sits outside any quotes, so the characters that matter are the space and the equals sign, and those are exactly the two it leaves alone. Applying it to a name is not partial protection; it is no protection.
The more transferable point is about which half of a key-value pair is trusted. Developers reason carefully about values, because values are understood to be user input. Keys are treated as structure, something the program chose. Here the key comes from a settings array that a page builder persists without validating, so the attacker chooses the structure too. That is the same shape as an earlier finding in this set where a sanitiser was keyed by attacker-controlled field names, and it is worth checking anywhere a loop emits $key into markup.
Disclosure timeline
- 2026-09-22 Vulnerability identified and validated by Intrudify, reported via Patchstack
- 2026-09-30 Public disclosure · CVE-2026-97301 assigned
Questions
What is CVE-2026-97301?
CVE-2026-97301 is a stored cross-site scripting vulnerability in the Cool FormKit Lite WordPress plugin, versions 2.7.8 and earlier. It was discovered autonomously by the Intrudify AI penetration testing engine. The Cool Form widget prints the keys of a stored settings array as HTML attribute names, and esc_attr does not encode spaces or equals signs, so one key becomes several attributes including an event handler.
Does the payload require the post to be published?
No. The payload executes as soon as an administrator or editor opens the preview of the pending submission, which is the ordinary review step for Contributor content, so the script runs inside an authenticated administrator session. Once published, it executes for every visitor.
How do I fix CVE-2026-97301?
Update Cool FormKit Lite to version 2.7.9 or later. The underlying fix is to stop treating stored setting keys as HTML attribute names, building the attribute list from a fixed allow-list or validating each key against a strict pattern, and to apply the same change to the editor template.