Feb 24, 2026·6 min read·14 visits
Craft CMS contained a 'ghost' column type called 'html' within its Table Field component. While not visible in the UI, it was supported by the backend. Attackers with field configuration permissions could manually set a column to this type, enabling the rendering of raw, unsanitized HTML (and JavaScript) in the admin panel.
A hidden, semi-internal 'html' column type in Craft CMS's Table Field component allowed authenticated administrators to inject arbitrary JavaScript. By manipulating field configuration requests, an attacker could enable this unexposed column type, bypassing sanitization and achieving Stored XSS against other control panel users.
We all love a flexible Content Management System. Craft CMS is arguably one of the best, giving developers granular control over data structures. One of its most powerful tools is the 'Table Field,' which allows content editors to manage rows of repeated data—text, numbers, checkboxes, etc. It is the Excel spreadsheet of the CMS world.
But here is the thing about software development: features get deprecated, hidden, or half-built, and sometimes they never really leave the codebase. They just become ghosts. In CVE-2026-27126, researchers found one of these ghosts lurking in the Table Field logic.
It turns out, while the UI only let you pick mundane column types like singleline or number, the backend logic was secretly holding a candle for a type called html. And unlike its siblings, the html type did exactly what it sounds like—it took whatever input you gave it and dumped it directly into the DOM, raw and wriggling. It’s like finding a secret menu item at a restaurant that serves you uncooked chicken.
The vulnerability stems from a classic disconnect between the User Interface and the Application Logic. The developers correctly assumed that users shouldn't be creating columns that render raw HTML, so they simply didn't include html in the dropdown menu of the Field Settings page. Case closed, right? Wrong.
The backend component responsible for validating the Table Field configuration (src/fields/Table.php) had a blind spot. It accepted the column configuration array and processed it, but it didn't strictly validate that the requested type was one of the publicly supported options.
Simultaneously, the frontend template responsible for rendering the table in the control panel (editableTable.twig) had logic explicitly designed to handle this html type. It was likely a remnant of an internal feature or a deprecated capability. When the renderer encountered a column with type: 'html', it skipped the usual output encoding and treated the cell's content as trusted markup. This created a perfect Stored XSS vector for anyone who could force the configuration to acknowledge this hidden type.
Let's look at the PHP code. Before the patch, the validateColumns method was a bit too permissible. It iterated through columns to check basic properties but failed to enforce a strict allowlist on the type attribute.
The fix, implemented by Brandon Kelly, introduces a hardcoded list of allowed types and forces anything else to degrade to a harmless singleline text field.
Before (Conceptual Logic):
foreach ($this->columns as &$col) {
// Checking label, handle, etc.
// No check if $col['type'] is actually valid!
}After (Patched Logic):
// src/fields/Table.php
private static function typeOptions(): array
{
return [
'checkbox' => true,
'color' => true,
'date' => true,
'select' => true,
// ... (other safe types)
'url' => true,
];
}
public function validateColumns(): void
{
$typeOptions = self::typeOptions();
foreach ($this->columns as &$col) {
// If the type isn't in our VIP list, you're getting a text box.
if (!isset($typeOptions[$col['type']])) {
$col['type'] = 'singleline';
}
// ...
}
}By adding this check, the 'ghost' html type is effectively exorcised. Even if an attacker tries to inject it, the system forces it into a singleline type, which escapes all output.
Since the UI won't let us select the html type, we have to get our hands dirty with a proxy. This is an 'Authenticated Admin' exploit, meaning the attacker needs permissions to configure fields (specifically allowAdminChanges must be on).
Here is the attack chain:
"type": "singleline"."type": "html". The backend, lacking the validation we saw earlier, saves this configuration to the database.<img src=x onerror=alert(document.cookie)> into the cell.editableTable.twig sees the html type and renders the script tag immediately.You might be thinking, "If I already have Admin access to change settings, why do I need XSS?" That is a fair question, but it misses the nuance of modern access control.
First, Privilege Escalation. In many organizations, there are tiers of administrators. A 'Site Builder' might have permission to configure fields but not to install plugins or execute PHP. By using this XSS, the lower-tier admin can hijack the session of a Super Admin. Once they have that session, they can install a malicious plugin or edit templates to achieve full Remote Code Execution (RCE) on the server.
Second, Persistence. An attacker who briefly compromises an admin account can plant this 'time bomb' in the database. Even if their original access is revoked, the XSS payload remains in the content, waiting for a high-value target to view the table. It is a backdoor that lives in the legitimate data structure of the site.
The remediation is straightforward: upgrade. The patch exists in Craft CMS 4.16.19 and 5.8.23. These versions introduce the strict type checking we analyzed above.
If you cannot upgrade immediately, there is a configuration-level mitigation. Craft CMS has a setting called allowAdminChanges. In a properly secured production environment, this should be set to false.
// config/general.php
'allowAdminChanges' => false,Disabling admin changes prevents anyone from modifying field configurations via the control panel, effectively neutralizing the attack vector (unless the attacker has direct database access, in which case you have bigger problems). This is a best practice for Craft CMS deployment pipelines anyway—treat your production schema as immutable.
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | >= 4.5.0-RC1, < 4.16.19 | 4.16.19 |
Craft CMS Pixel & Tonic | >= 5.0.0-RC1, < 5.8.23 | 5.8.23 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (Authenticated) |
| CVSS v4.0 | 5.9 (Medium) |
| Impact | Stored XSS / Session Hijacking |
| Privileges Required | Low (Admin with Settings Access) |
| Exploit Status | PoC Available |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')