Aug 21, 2026·6 min read·44 visits
An incomplete sanitization flow in Winter CMS caches raw, unsanitized custom CSS styles. On cache hits, the raw styles are output directly into the backend interface without sanitization, allowing attackers with branding configuration permissions to achieve administrative privilege escalation via Stored XSS.
Winter CMS versions prior to 1.2.14 are vulnerable to Stored Cross-Site Scripting (XSS) within the administrative backend interface. The flaw resides in the custom styles rendering pipeline for Brand Settings and Editor Settings. An attacker with privileges to modify backend branding or editor configurations can inject arbitrary JavaScript, which is written to the cache without sanitization. Subsequent page requests that result in a cache hit completely bypass output sanitization filters, leading to JavaScript execution in the sessions of other administrative users.
Winter CMS is an open-source content management system built on the Laravel framework. It provides administrative controls allowing high-privilege users to customize the appearance of the backend user interface. These customization controls are managed via the BrandSetting and EditorSetting components, which allow administrators to input and apply custom LESS and CSS style declarations.
An improper input neutralization vulnerability (CWE-79) exists in the way these custom CSS inputs are stored, compiled, cached, and rendered to backend users. Specifically, the rendering pipeline fails to ensure that style configurations retrieved from the caching layer undergo sanitization before being served to the client browser.
Because these style declarations are rendered directly into the HTML within administrative style blocks, an attacker who has permission to modify branding or editor settings can inject malicious HTML and JavaScript. When other administrative users access the backend interface, the browser parses the unescaped payload, leading to stored cross-site scripting (XSS) in the context of the victim's session.
The root cause of this vulnerability lies in the caching logic of the renderCss() method within both BrandSetting.php and EditorSetting.php. While the system was designed to sanitize custom CSS output using PHP's strip_tags() function during active compilation, the application fails to enforce this sanitization boundary when serving cached content.
When a request triggers a cache miss, the compiler compiles the custom LESS/CSS declarations. This compiled, raw string is stored directly in the cache forever using Cache::forever($cacheKey, $customCss). This creates a poisoned cache state where the raw, unescaped payload resides within the cache database.
When a subsequent page request is made, the application hits the cache via Cache::has($cacheKey). Instead of sanitizing the cached data, the system immediately returns the raw value via Cache::get($cacheKey). This architecture allows the raw, malicious payload to bypass output encoding and sanitization filters entirely on every subsequent page load.
An analysis of the vulnerable implementation in modules/backend/models/BrandSetting.php reveals the exact point where the validation filter is bypassed. The vulnerable renderCss implementation is structured as follows:
// Vulnerable Implementation in Winter CMS <= 1.2.13
public static function renderCss()
{
$cacheKey = self::instance()->cacheKey;
if (Cache::has($cacheKey)) {
// BUG: Directly returns the raw cached string without validation
return Cache::get($cacheKey);
}
try {
$customCss = self::compileCss();
// The raw compiled string is saved forever to the cache database
Cache::forever($cacheKey, $customCss);
}
catch (Exception $ex) {
$customCss = '/* ' . e($ex->getMessage()) . ' */';
}
// Sanitization is only applied to the cache-miss return path
return strip_tags($customCss);
}In the patched version, developers corrected the flow by wrapping the cache retrieval logic in a strip_tags() filter. This ensures that even if an existing cache entry contains malicious tags, they are stripped before being injected into the HTML response:
// Patched Implementation in Winter CMS v1.2.14
public static function renderCss()
{
$cacheKey = self::instance()->cacheKey;
if (Cache::has($cacheKey)) {
// FIX: Sanitization is now applied to cache hits
return strip_tags(Cache::get($cacheKey));
}
try {
$customCss = self::compileCss();
Cache::forever($cacheKey, $customCss);
} catch (Exception $ex) {
$customCss = '/* ' . e($ex->getMessage()) . ' */';
}
return strip_tags($customCss);
}By applying the mitigation at the point of cache retrieval, the application effectively neutralizes both newly compiled payloads and legacy, pre-existing poisoned cache entries that might reside in the database prior to the upgrade.
To exploit this vulnerability, an attacker must first obtain credentials for a backend user account that possesses either the backend.manage_branding or backend.manage_editor permissions. These permissions allow the customization of layout configurations and custom styles.
The attacker crafts a stylesheet payload. To bypass simple validation on saving, the attacker utilizes the LESS compiler's escape character (~) or standard syntax structures that are interpreted as strings by the compiler but escape the HTML context when output:
/* Brand Custom CSS Payload */
.x { content: ~"</style><script>alert('XSS-Exploited')</script><style>"; }Alternatively, for the editor stylesheet configuration, the following payload achieves the same result:
/* Editor Custom CSS Payload */
.fr-view .x { content: </style><script>alert('XSS-Exploited')</script><style>; }When these settings are saved, the backend cache is cleared and then primed on the next page request. The compiler parses the stylesheet and stores the raw, unescaped string in the cache database. When any subsequent administrative user logs in and navigates the backend dashboard, the system retrieves the raw cache value, inserting it directly into the dashboard <style> block. The victim's browser parses the </style> tag, terminates the CSS context, and immediately executes the injected JavaScript.
The impact of this Stored XSS vulnerability is critical. In a typical Winter CMS installation, backend administrators have extensive controls over the application, including the ability to manage system settings, users, and even raw PHP code execution through template editors if the developer tools are enabled.
An attacker who successfully executes arbitrary JavaScript within the session of an active administrator can capture session tokens, extract anti-CSRF tokens, or execute administrative actions on behalf of the victim. This enables an attacker with restricted branding management privileges to elevate their access to a full super-administrator or system developer role.
In environments where template-editing features are exposed in the backend, administrative privilege escalation directly leads to remote code execution (RCE) on the underlying server. This chain of execution allows the attacker to execute arbitrary commands, access sensitive databases, or achieve complete host compromise.
Remediation requires upgrading the system to Winter CMS version 1.2.14 or later. This can be accomplished by running composer update wintercms/winter inside the project root directory. In addition to the code patch, administrators should verify that backend administrative roles are properly audited.
To identify potential compromise or existing poisoned configurations on legacy systems, administrators can inspect the system_settings database table. The following SQL query searches for style records containing raw script or closing style tags:
SELECT * FROM system_settings
WHERE item = 'backend_brand_settings'
AND (value LIKE '%</style>%' OR value LIKE '%<script>%');Furthermore, Web Application Firewalls (WAF) can be configured to detect and block incoming request bodies containing escape attempts directed at CSS customization endpoints. Custom regex signatures should monitor backend parameters such as custom_css or html_custom_styles for the presence of closing HTML tags or script elements.
CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Winter CMS Winter CMS | < 1.2.14 | 1.2.14 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Secondary CWE ID | CWE-524 |
| Attack Vector | Network |
| CVSS v3.1 Score | 8.4 (High) |
| Exploit Status | PoC / Regression Tested |
| KEV Status | Not Listed |
| Ransomware Association | No |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Payload CMS is subject to an information disclosure vulnerability where users with query permissions can bypass field-level access controls. By leveraging polymorphic join filters, an attacker can perform blind-inference queries to retrieve restricted or hidden fields such as password reset tokens.
An open redirect vulnerability exists in Payload CMS within its Next.js-based authentication routing components. The sanitization utility fails to properly account for control characters and ambiguous encodings, allowing unauthenticated attackers to redirect users to external malicious domains after successful authentication.
A critical SQL Injection and access control bypass vulnerability was identified in Payload CMS database adapters (SQLite and PostgreSQL using Drizzle ORM internally). The vulnerability arises from case-sensitive logical operator checks during path validation and unvalidated sort queries. This allows remote attackers to bypass access control rules, execute unauthorized queries, and retrieve sensitive data through blind SQL injection side channels.
A critical prototype pollution vulnerability in the import-export plugin of Payload CMS allows unauthenticated remote attackers to bypass access controls and achieve remote code execution.
CVE-2026-105806 is an improper access control vulnerability within the Model Context Protocol (MCP) plugin for Payload CMS. Authenticated users with low privileges can manipulate API key creation and mapping to associate keys with arbitrary users, including administrators. This allows total session takeovers and privilege escalation via MCP-authenticated API requests.
An access control vulnerability in `@payloadcms/plugin-stripe` allows authenticated low-privilege users to bypass authorization boundaries and execute arbitrary, highly privileged operations on the connected Stripe platform via an exposed REST proxy.