Introduction
A missing file type validation in the Breeze Cache plugin for WordPress allows unauthenticated attackers to upload arbitrary files to the server, opening the door to remote code execution on any site with a specific caching feature enabled. With over 400,000 active installations and 14 million total downloads, Breeze Cache is one of the more popular performance optimization plugins in the WordPress ecosystem, making this CVSS 9.8 vulnerability a significant concern for the WordPress community.
Breeze Cache is developed by Cloudways and provides page caching, file minification, CDN integration, and local hosting of external assets (including Gravatars) to improve site loading performance. Its wide adoption across shared hosting, VPS, and managed WordPress environments means that even a conditionally exploitable flaw like this one has a meaningful blast radius.
Technical Information
Root Cause: Zero Validation in fetch_gravatar_from_remote()
The vulnerability lives in the fetch_gravatar_from_remote() function within inc/class-breeze-cache-cronjobs.php. In versions 2.4.4 and earlier, this function is responsible for downloading Gravatar images from remote sources and caching them locally. The function accepts a URL, calls the WordPress download_url method to fetch whatever that URL serves, and then moves the downloaded content into a web accessible cache directory using the WordPress filesystem move method.
The critical flaw is that none of these steps include any form of validation. There is no check on the origin domain of the URL, no inspection of the file extension, and no verification of the downloaded file's actual content type. The function blindly trusts whatever URL it receives.
The Upstream Caller: breeze_replace_gravatar_image()
The breeze_replace_gravatar_image() method parses HTML src and srcset attributes to extract Gravatar URLs, which are then passed to fetch_gravatar_from_remote(). In the vulnerable version, the regex used for this extraction was overly permissive:
// Old (vulnerable): matched unquoted/loosely quoted attribute values preg_match_all( '/srcset=["\']?(...)["\']?/', $gravatar, $srcset );
This loose pattern could be tricked into matching attacker controlled strings embedded in other HTML attributes (such as a malicious alt value), expanding the URL injection surface.
Attack Flow
The exploitation path proceeds through these steps:
- The attacker identifies a WordPress site running Breeze Cache with the "Host Files Locally Gravatars" option enabled.
- The attacker crafts input containing a URL pointing to a malicious file hosted on an attacker controlled server. This could be achieved by controlling a comment author's profile data or by manipulating HTML attributes that the plugin's regex parses.
- The
breeze_replace_gravatar_image()method extracts the attacker supplied URL from the HTML. - The URL is passed to
fetch_gravatar_from_remote(), which downloads the file (for example, a PHP webshell) without any validation. - The downloaded file is moved to the publicly reachable cache directory at
WP_CONTENT_DIR/cache/breeze extra/gravatars/. - The attacker requests the file directly via its public URL, achieving remote code execution on the server.
Gating Condition
Exploitation is entirely dependent on the "Host Files Locally Gravatars" setting being enabled. This feature is disabled by default, which limits the immediate attack surface to sites that have explicitly turned it on for performance reasons. For those sites, however, the risk is immediate: the vulnerability requires no authentication and can be triggered remotely.
Patch Information
The vulnerability was patched in Breeze Cache version 2.4.5, released on April 21, 2026 via WordPress plugin repository changeset 3511463. The fix applies a comprehensive, multi layered defense to both fetch_gravatar_from_remote() and its upstream caller breeze_replace_gravatar_image().
Layer 1: Origin Domain Allowlist
At the top of fetch_gravatar_from_remote(), the patch now extracts and validates the hostname of the incoming URL. Only gravatar.com and its subdomains are accepted:
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); if ( 'gravatar.com' !== $host && '.gravatar.com' !== substr( $host, -13 ) ) { return $url; }
This is the most impactful single change. It closes off the SSRF like vector entirely, preventing the plugin from ever fetching content from attacker controlled servers.
Layer 2: Pre Download File Extension Validation
Before any network request is made, the patch calls wp_check_filetype() on the URL's basename and compares it against an explicit allowlist of image MIME types. If the filename has a recognized extension that maps to a non image MIME type (such as .php or .html), the request is rejected. If there is no recognizable extension at all, the filename is safely normalized by appending .jpg:
$filetype = wp_check_filetype( $gravatar_name ); $allowed_images = array( 'image/jpeg', 'image/png', 'image/gif' ); if ( ! empty( $filetype['type'] ) && ! in_array( $filetype['type'], $allowed_images, true ) ) { return $url; } if ( empty( $filetype['ext'] ) || ! in_array( $filetype['type'], $allowed_images, true ) ) { $gravatar_name .= '.jpg'; }
Layer 3: Post Download Content Verification
Even after a file is downloaded to a temporary location, the patch does not trust it. It invokes wp_check_filetype_and_ext(), which inspects the actual file contents rather than just the name, and confirms the MIME type starts with image/. If the downloaded file fails this content based check, it is immediately deleted:
$file_check = wp_check_filetype_and_ext( $temp_gravatar, $gravatar_name ); if ( empty( $file_check['type'] ) || 0 !== strpos( $file_check['type'], 'image/' ) ) { @unlink( $temp_gravatar ); return $url; }
This is critical defense in depth: even if an attacker managed to bypass the domain and extension checks, a file with non image content (such as a PHP webshell) would be caught here and deleted before it can be moved to the final web accessible path.
Layer 4: Input Regex Hardening in the Caller
The breeze_replace_gravatar_image() method's regex was tightened to require whitespace before the attribute and properly quoted values, reducing the URL injection surface:
// New (patched): requires whitespace before attribute, properly quoted values preg_match( '/\ssrcset=["\']([^"\']+)["\']/', $gravatar, $srcset_match );
Additionally, error handling was refactored from a "proceed on success" nesting pattern to early return on failure, and the temporary file is now always cleaned up with @unlink regardless of whether the move succeeded, eliminating potential accumulation of orphaned temp files.
Affected Systems and Versions
All versions of the Breeze Cache plugin for WordPress up to and including version 2.4.4 are affected. The vulnerability is only exploitable when the "Host Files Locally Gravatars" configuration option is enabled; this setting is disabled by default.
The fix is available in version 2.4.5 and later.
Vendor Security History
Wordfence intelligence records 9 distinct vulnerabilities for the Breeze plugin since 2022. Notable entries include:
| Vulnerability Type | CVE Identifier | CVSS Score | Status |
|---|---|---|---|
| Unauthenticated Arbitrary File Upload | CVE 2026 3844 | 9.8 | Patched in 2.4.5 |
| Missing Authorization to Cache Deletion | CVE 2025 13864 | 5.3 | Patched |
| Missing Authorization | CVE 2025 69364 | 5.3 | Patched |
| Authenticated Stored Cross Site Scripting | CVE 2024 50431 | 4.4 | Patched |
While Cloudways has been responsive in issuing patches for reported vulnerabilities, the recurring pattern of authorization and validation flaws indicates that organizations running Breeze Cache should treat plugin updates as a priority maintenance item.
References
- NVD Entry for CVE-2026-3844
- Wordfence Threat Intel Advisory
- WordPress Plugin Repository Changeset 3511463
- Patched Source: class-breeze-cache-cronjobs.php (v2.4.5)
- Vulnerable Source: class-breeze-cache-cronjobs.php (v2.4.4)
- Vulnerable Source: class-breeze-cache-cronjobs.php (v2.4.1, Line 119)
- Vulnerable Source: class-breeze-cache-cronjobs.php (v2.4.1, Line 89)
- Wordfence Breeze Plugin Vulnerability History



